@vedmalex/statemachine 1.0.0-beta.1
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 -0
- package/README.md +75 -0
- package/dist/index.cjs +3989 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +3943 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/types/adapters.d.ts +30 -0
- package/types/config_validator.d.ts +76 -0
- package/types/error_handling.d.ts +124 -0
- package/types/index.d.ts +77 -0
- package/types/lite.d.ts +6 -0
- package/types/logger.d.ts +74 -0
- package/types/monitoring.d.ts +239 -0
- package/types/presets.d.ts +27 -0
- package/types/scheduler.d.ts +65 -0
- package/types/security.d.ts +145 -0
- package/types/state_machine.d.ts +227 -0
- package/types/types.d.ts +250 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3989 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
EnhancedErrorHandler: () => ErrorHandler,
|
|
24
|
+
EnhancedStateMachineError: () => EnhancedStateMachineError,
|
|
25
|
+
ErrorAnalytics: () => ErrorAnalytics,
|
|
26
|
+
ErrorCategory: () => ErrorCategory,
|
|
27
|
+
ErrorSeverity: () => ErrorSeverity,
|
|
28
|
+
FallbackStateRecoveryStrategy: () => FallbackStateRecoveryStrategy,
|
|
29
|
+
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
30
|
+
MemoryAdapter: () => MemoryAdapter,
|
|
31
|
+
RetryRecoveryStrategy: () => RetryRecoveryStrategy,
|
|
32
|
+
ServerAdapter: () => ServerAdapter,
|
|
33
|
+
SessionStorageAdapter: () => SessionStorageAdapter,
|
|
34
|
+
StateMachine: () => StateMachine,
|
|
35
|
+
StateMachineError: () => StateMachineError,
|
|
36
|
+
createEnhancedError: () => createEnhancedError,
|
|
37
|
+
createMachine: () => createMachine,
|
|
38
|
+
isAdapter: () => isAdapter,
|
|
39
|
+
isRecoverableError: () => isRecoverableError,
|
|
40
|
+
isValidConfig: () => isValidConfig,
|
|
41
|
+
validateConfig: () => validateConfig,
|
|
42
|
+
validateConfigStrict: () => validateConfigStrict
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/scheduler.ts
|
|
47
|
+
var TimerScheduler = class {
|
|
48
|
+
// Min-Heap: элемент с наименьшим executeAt всегда в index 0
|
|
49
|
+
heap = [];
|
|
50
|
+
// Set для быстрой проверки отмененных задач (Lazy Cancellation)
|
|
51
|
+
// Это позволяет удалять задачи за O(1) без поиска в куче O(n)
|
|
52
|
+
activeTokens = /* @__PURE__ */ new WeakSet();
|
|
53
|
+
intervalId = null;
|
|
54
|
+
pollingInterval = 100;
|
|
55
|
+
// ms
|
|
56
|
+
constructor() {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Настройка режима опроса
|
|
60
|
+
* @param interval Интервал проверки в мс. Если 0 или null - авто-тик выключается (ручной режим)
|
|
61
|
+
*/
|
|
62
|
+
setPollingInterval(interval) {
|
|
63
|
+
this.stop();
|
|
64
|
+
if (interval !== null && interval > 0) {
|
|
65
|
+
this.pollingInterval = interval;
|
|
66
|
+
this.start();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Проверка активности планировщика
|
|
71
|
+
*/
|
|
72
|
+
isActive() {
|
|
73
|
+
return this.intervalId !== null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Запуск единого таймера
|
|
77
|
+
*/
|
|
78
|
+
start() {
|
|
79
|
+
if (this.intervalId) return;
|
|
80
|
+
this.intervalId = setInterval(() => this.process(), this.pollingInterval);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Остановка единого таймера
|
|
84
|
+
*/
|
|
85
|
+
stop() {
|
|
86
|
+
if (this.intervalId) {
|
|
87
|
+
clearInterval(this.intervalId);
|
|
88
|
+
this.intervalId = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Добавить задачу
|
|
93
|
+
* @param delay задержка в мс
|
|
94
|
+
* @param callback функция для выполнения
|
|
95
|
+
* @returns токен для отмены
|
|
96
|
+
*/
|
|
97
|
+
schedule(delay, callback) {
|
|
98
|
+
const token = {};
|
|
99
|
+
const executeAt = Date.now() + delay;
|
|
100
|
+
const task = { token, executeAt, callback };
|
|
101
|
+
this.activeTokens.add(token);
|
|
102
|
+
this.insert(task);
|
|
103
|
+
return token;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Отменить задачу
|
|
107
|
+
* @param token токен задачи
|
|
108
|
+
*/
|
|
109
|
+
cancel(token) {
|
|
110
|
+
this.activeTokens.delete(token);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Обработать очередь (вызывается таймером или вручную)
|
|
114
|
+
*/
|
|
115
|
+
process(now = Date.now()) {
|
|
116
|
+
while (this.heap.length > 0) {
|
|
117
|
+
const task = this.heap[0];
|
|
118
|
+
if (task === void 0) break;
|
|
119
|
+
if (task.executeAt > now) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
this.extractMin();
|
|
123
|
+
if (this.activeTokens.has(task.token)) {
|
|
124
|
+
this.activeTokens.delete(task.token);
|
|
125
|
+
try {
|
|
126
|
+
task.callback();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error("Error in scheduled task", e);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Очистить все задачи
|
|
135
|
+
*/
|
|
136
|
+
clear() {
|
|
137
|
+
this.heap = [];
|
|
138
|
+
this.activeTokens = /* @__PURE__ */ new WeakSet();
|
|
139
|
+
}
|
|
140
|
+
// === Min-Heap Implementation ===
|
|
141
|
+
insert(task) {
|
|
142
|
+
this.heap.push(task);
|
|
143
|
+
this.bubbleUp(this.heap.length - 1);
|
|
144
|
+
}
|
|
145
|
+
extractMin() {
|
|
146
|
+
if (this.heap.length === 0) return void 0;
|
|
147
|
+
const min = this.heap[0];
|
|
148
|
+
if (min === void 0) return void 0;
|
|
149
|
+
const last = this.heap.pop();
|
|
150
|
+
if (this.heap.length > 0 && last !== void 0) {
|
|
151
|
+
this.heap[0] = last;
|
|
152
|
+
this.sinkDown(0);
|
|
153
|
+
}
|
|
154
|
+
return min;
|
|
155
|
+
}
|
|
156
|
+
bubbleUp(index) {
|
|
157
|
+
while (index > 0) {
|
|
158
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
159
|
+
const parent = this.heap[parentIndex];
|
|
160
|
+
const current = this.heap[index];
|
|
161
|
+
if (parent === void 0 || current === void 0) break;
|
|
162
|
+
if (parent.executeAt <= current.executeAt) break;
|
|
163
|
+
this.swap(index, parentIndex);
|
|
164
|
+
index = parentIndex;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
sinkDown(index) {
|
|
168
|
+
const length = this.heap.length;
|
|
169
|
+
const element = this.heap[index];
|
|
170
|
+
if (element === void 0) return;
|
|
171
|
+
while (true) {
|
|
172
|
+
const leftChildIdx = 2 * index + 1;
|
|
173
|
+
const rightChildIdx = 2 * index + 2;
|
|
174
|
+
let swapIdx = null;
|
|
175
|
+
if (leftChildIdx < length) {
|
|
176
|
+
const leftChild = this.heap[leftChildIdx];
|
|
177
|
+
if (leftChild !== void 0 && leftChild.executeAt < element.executeAt) {
|
|
178
|
+
swapIdx = leftChildIdx;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (rightChildIdx < length) {
|
|
182
|
+
const rightChild = this.heap[rightChildIdx];
|
|
183
|
+
if (rightChild !== void 0) {
|
|
184
|
+
if (swapIdx === null && rightChild.executeAt < element.executeAt || /* c8 ignore next */
|
|
185
|
+
swapIdx !== null && this.heap[swapIdx] !== void 0 && rightChild.executeAt < this.heap[swapIdx].executeAt) {
|
|
186
|
+
swapIdx = rightChildIdx;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (swapIdx === null) break;
|
|
191
|
+
this.swap(index, swapIdx);
|
|
192
|
+
index = swapIdx;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
swap(a, b) {
|
|
196
|
+
const temp = this.heap[a];
|
|
197
|
+
const other = this.heap[b];
|
|
198
|
+
if (temp === void 0 || other === void 0) return;
|
|
199
|
+
this.heap[a] = other;
|
|
200
|
+
this.heap[b] = temp;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function createDefaultScheduler() {
|
|
204
|
+
return new TimerScheduler();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/logger.ts
|
|
208
|
+
var LogLevel = {
|
|
209
|
+
DEBUG: 0,
|
|
210
|
+
INFO: 1,
|
|
211
|
+
WARN: 2,
|
|
212
|
+
ERROR: 3,
|
|
213
|
+
FATAL: 4,
|
|
214
|
+
OFF: 5
|
|
215
|
+
};
|
|
216
|
+
var LogLevelNames = {
|
|
217
|
+
[LogLevel.DEBUG]: "DEBUG",
|
|
218
|
+
[LogLevel.INFO]: "INFO",
|
|
219
|
+
[LogLevel.WARN]: "WARN",
|
|
220
|
+
[LogLevel.ERROR]: "ERROR",
|
|
221
|
+
[LogLevel.FATAL]: "FATAL",
|
|
222
|
+
[LogLevel.OFF]: "OFF"
|
|
223
|
+
};
|
|
224
|
+
var DEFAULT_LOGGER_CONFIG = {
|
|
225
|
+
level: LogLevel.WARN,
|
|
226
|
+
// Only warnings and above in production
|
|
227
|
+
enableConsole: true,
|
|
228
|
+
enableStructuredLogging: false,
|
|
229
|
+
timestampFormat: "iso",
|
|
230
|
+
includeStackTrace: false
|
|
231
|
+
};
|
|
232
|
+
var ConsoleAppender = class {
|
|
233
|
+
config;
|
|
234
|
+
constructor(config) {
|
|
235
|
+
this.config = config;
|
|
236
|
+
}
|
|
237
|
+
append(entry) {
|
|
238
|
+
if (!this.config.enableConsole) return;
|
|
239
|
+
const timestamp = this.formatTimestamp(entry.timestamp);
|
|
240
|
+
const levelName = LogLevelNames[entry.level];
|
|
241
|
+
const prefix = `[${timestamp}] [${levelName}] [${entry.source}]`;
|
|
242
|
+
if (this.config.enableStructuredLogging) {
|
|
243
|
+
const structuredEntry = {
|
|
244
|
+
timestamp: entry.timestamp,
|
|
245
|
+
level: levelName,
|
|
246
|
+
source: entry.source,
|
|
247
|
+
message: entry.message,
|
|
248
|
+
context: entry.context,
|
|
249
|
+
error: entry.error?.message
|
|
250
|
+
};
|
|
251
|
+
console.log(JSON.stringify(structuredEntry));
|
|
252
|
+
} else {
|
|
253
|
+
const message = `${prefix} ${entry.message}`;
|
|
254
|
+
switch (entry.level) {
|
|
255
|
+
case LogLevel.DEBUG:
|
|
256
|
+
case LogLevel.INFO:
|
|
257
|
+
console.log(message, entry.context || "");
|
|
258
|
+
break;
|
|
259
|
+
case LogLevel.WARN:
|
|
260
|
+
console.warn(message, entry.context || "");
|
|
261
|
+
break;
|
|
262
|
+
case LogLevel.ERROR:
|
|
263
|
+
case LogLevel.FATAL:
|
|
264
|
+
console.error(message, entry.context || "", entry.error || "");
|
|
265
|
+
if (this.config.includeStackTrace && entry.error) {
|
|
266
|
+
console.error(entry.error.stack);
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
formatTimestamp(timestamp) {
|
|
273
|
+
switch (this.config.timestampFormat) {
|
|
274
|
+
case "unix":
|
|
275
|
+
return timestamp.toString();
|
|
276
|
+
case "relative":
|
|
277
|
+
return `+${timestamp - Date.now()}ms`;
|
|
278
|
+
case "iso":
|
|
279
|
+
default:
|
|
280
|
+
return new Date(timestamp).toISOString();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var Logger = class _Logger {
|
|
285
|
+
config;
|
|
286
|
+
appenders = [];
|
|
287
|
+
source;
|
|
288
|
+
constructor(source, config = {}) {
|
|
289
|
+
this.source = source;
|
|
290
|
+
this.config = { ...DEFAULT_LOGGER_CONFIG, ...config };
|
|
291
|
+
this.appenders.push(new ConsoleAppender(this.config));
|
|
292
|
+
}
|
|
293
|
+
// Add custom appender
|
|
294
|
+
addAppender(appender) {
|
|
295
|
+
this.appenders.push(appender);
|
|
296
|
+
}
|
|
297
|
+
// Remove appender
|
|
298
|
+
removeAppender(appender) {
|
|
299
|
+
const index = this.appenders.indexOf(appender);
|
|
300
|
+
if (index > -1) {
|
|
301
|
+
this.appenders.splice(index, 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Update configuration
|
|
305
|
+
updateConfig(config) {
|
|
306
|
+
this.config = { ...this.config, ...config };
|
|
307
|
+
}
|
|
308
|
+
// Check if level is enabled
|
|
309
|
+
isLevelEnabled(level) {
|
|
310
|
+
return level >= this.config.level && this.config.level !== LogLevel.OFF;
|
|
311
|
+
}
|
|
312
|
+
// Core logging method
|
|
313
|
+
log(level, message, context, error) {
|
|
314
|
+
if (!this.isLevelEnabled(level)) return;
|
|
315
|
+
const entry = {
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
level,
|
|
318
|
+
message,
|
|
319
|
+
...context !== void 0 ? { context } : {},
|
|
320
|
+
...error !== void 0 ? { error } : {},
|
|
321
|
+
source: this.source
|
|
322
|
+
};
|
|
323
|
+
this.appenders.forEach((appender) => {
|
|
324
|
+
try {
|
|
325
|
+
appender.append(entry);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error("Logger appender failed:", e);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// Convenience methods
|
|
332
|
+
debug(message, context) {
|
|
333
|
+
this.log(LogLevel.DEBUG, message, context);
|
|
334
|
+
}
|
|
335
|
+
info(message, context) {
|
|
336
|
+
this.log(LogLevel.INFO, message, context);
|
|
337
|
+
}
|
|
338
|
+
warn(message, context) {
|
|
339
|
+
this.log(LogLevel.WARN, message, context);
|
|
340
|
+
}
|
|
341
|
+
error(message, context, error) {
|
|
342
|
+
this.log(LogLevel.ERROR, message, context, error);
|
|
343
|
+
}
|
|
344
|
+
fatal(message, context, error) {
|
|
345
|
+
this.log(LogLevel.FATAL, message, context, error);
|
|
346
|
+
}
|
|
347
|
+
// Create child logger with additional context
|
|
348
|
+
child(childSource, _additionalContext) {
|
|
349
|
+
const childLogger = new _Logger(`${this.source}.${childSource}`, this.config);
|
|
350
|
+
childLogger.appenders = this.appenders;
|
|
351
|
+
return childLogger;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var LoggerFactory = class _LoggerFactory {
|
|
355
|
+
static defaultConfig = DEFAULT_LOGGER_CONFIG;
|
|
356
|
+
static loggers = /* @__PURE__ */ new Map();
|
|
357
|
+
static setDefaultConfig(config) {
|
|
358
|
+
_LoggerFactory.defaultConfig = { ...DEFAULT_LOGGER_CONFIG, ...config };
|
|
359
|
+
}
|
|
360
|
+
static getLogger(source, config) {
|
|
361
|
+
const key = source;
|
|
362
|
+
if (!_LoggerFactory.loggers.has(key)) {
|
|
363
|
+
const loggerConfig = config ? { ..._LoggerFactory.defaultConfig, ...config } : _LoggerFactory.defaultConfig;
|
|
364
|
+
_LoggerFactory.loggers.set(key, new Logger(source, loggerConfig));
|
|
365
|
+
}
|
|
366
|
+
return _LoggerFactory.loggers.get(key);
|
|
367
|
+
}
|
|
368
|
+
static clearLoggers() {
|
|
369
|
+
_LoggerFactory.loggers.clear();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var getLogger = LoggerFactory.getLogger;
|
|
373
|
+
var stateMachineLogger = getLogger("StateMachine");
|
|
374
|
+
var securityLogger = getLogger("Security");
|
|
375
|
+
|
|
376
|
+
// src/security.ts
|
|
377
|
+
var BUILTIN_PATTERNS = [
|
|
378
|
+
{
|
|
379
|
+
pattern: /\beval\s*\(/gi,
|
|
380
|
+
risk: "critical",
|
|
381
|
+
description: "Direct eval() usage",
|
|
382
|
+
category: "eval"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
pattern: /\bFunction\s*\(/gi,
|
|
386
|
+
risk: "high",
|
|
387
|
+
description: "Dynamic Function constructor call",
|
|
388
|
+
category: "eval"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
pattern: /new\s+Function\s*\(/gi,
|
|
392
|
+
risk: "high",
|
|
393
|
+
description: "Dynamic function creation",
|
|
394
|
+
category: "eval"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
pattern: /setTimeout\s*\(\s*['"`][^'"`]*['"`]/gi,
|
|
398
|
+
risk: "high",
|
|
399
|
+
description: "setTimeout with string code",
|
|
400
|
+
category: "eval"
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
pattern: /setInterval\s*\(\s*['"`][^'"`]*['"`]/gi,
|
|
404
|
+
risk: "high",
|
|
405
|
+
description: "setInterval with string code",
|
|
406
|
+
category: "eval"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
pattern: /\bfetch\s*\(/gi,
|
|
410
|
+
risk: "medium",
|
|
411
|
+
description: "Network request via fetch",
|
|
412
|
+
category: "network"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
pattern: /XMLHttpRequest/gi,
|
|
416
|
+
risk: "medium",
|
|
417
|
+
description: "XMLHttpRequest usage",
|
|
418
|
+
category: "network"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
pattern: /WebSocket/gi,
|
|
422
|
+
risk: "medium",
|
|
423
|
+
description: "WebSocket connection",
|
|
424
|
+
category: "network"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
pattern: /require\s*\(\s*['"`]fs['"`]\s*\)/gi,
|
|
428
|
+
risk: "high",
|
|
429
|
+
description: 'File system access via require("fs")',
|
|
430
|
+
category: "filesystem"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
pattern: /import.*from\s*['"`]fs['"`]/gi,
|
|
434
|
+
risk: "high",
|
|
435
|
+
description: "File system access via import",
|
|
436
|
+
category: "filesystem"
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
pattern: /document\./gi,
|
|
440
|
+
risk: "medium",
|
|
441
|
+
description: "DOM access",
|
|
442
|
+
category: "dom"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
pattern: /window\./gi,
|
|
446
|
+
risk: "medium",
|
|
447
|
+
description: "Window object access",
|
|
448
|
+
category: "dom"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
pattern: /innerHTML/gi,
|
|
452
|
+
risk: "high",
|
|
453
|
+
description: "innerHTML manipulation (XSS risk)",
|
|
454
|
+
category: "dom"
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
pattern: /global\./gi,
|
|
458
|
+
risk: "high",
|
|
459
|
+
description: "Global scope access",
|
|
460
|
+
category: "global"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
pattern: /process\./gi,
|
|
464
|
+
risk: "high",
|
|
465
|
+
description: "Process object access",
|
|
466
|
+
category: "process"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
pattern: /child_process/gi,
|
|
470
|
+
risk: "critical",
|
|
471
|
+
description: "Child process execution",
|
|
472
|
+
category: "process"
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
pattern: /exec\s*\(/gi,
|
|
476
|
+
risk: "critical",
|
|
477
|
+
description: "Command execution",
|
|
478
|
+
category: "process"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
pattern: /require\s*\(/gi,
|
|
482
|
+
risk: "critical",
|
|
483
|
+
description: "CommonJS require",
|
|
484
|
+
category: "module"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
pattern: /import\s*\(/gi,
|
|
488
|
+
risk: "critical",
|
|
489
|
+
description: "Dynamic import",
|
|
490
|
+
category: "module"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
pattern: /__proto__/gi,
|
|
494
|
+
risk: "critical",
|
|
495
|
+
description: "Prototype pollution",
|
|
496
|
+
category: "prototype"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
pattern: /constructor/gi,
|
|
500
|
+
risk: "critical",
|
|
501
|
+
description: "Constructor access",
|
|
502
|
+
category: "prototype"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
pattern: /prototype/gi,
|
|
506
|
+
risk: "critical",
|
|
507
|
+
description: "Prototype access",
|
|
508
|
+
category: "prototype"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
pattern: /\[['"]__proto__['"]\]/gi,
|
|
512
|
+
risk: "critical",
|
|
513
|
+
description: "Prototype pollution via bracket notation",
|
|
514
|
+
category: "prototype"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
pattern: /constructor\s*\[/gi,
|
|
518
|
+
risk: "critical",
|
|
519
|
+
description: "Constructor access via bracket notation",
|
|
520
|
+
category: "prototype"
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
var DEFAULT_SECURITY_CONFIG = {
|
|
524
|
+
allowedFunctionNames: /* @__PURE__ */ new Set([
|
|
525
|
+
// Common safe function patterns
|
|
526
|
+
"onEnter",
|
|
527
|
+
"onExit",
|
|
528
|
+
"onBeforeEnter",
|
|
529
|
+
"onAfterEnter",
|
|
530
|
+
"onBeforeExit",
|
|
531
|
+
"onAfterExit",
|
|
532
|
+
"onTransition",
|
|
533
|
+
"onError",
|
|
534
|
+
"guard",
|
|
535
|
+
"onBefore",
|
|
536
|
+
"onAfter",
|
|
537
|
+
"onSuccess",
|
|
538
|
+
// Internal serializer function names (used as metadata)
|
|
539
|
+
"config_onError",
|
|
540
|
+
"state_onError",
|
|
541
|
+
"event_onBefore",
|
|
542
|
+
"event_onAfter",
|
|
543
|
+
"event_onSuccess",
|
|
544
|
+
"event_onError",
|
|
545
|
+
"transition_guard",
|
|
546
|
+
"transition_onTransition",
|
|
547
|
+
"transition_onError",
|
|
548
|
+
"invoke_cond",
|
|
549
|
+
"invoke_action",
|
|
550
|
+
// Legacy deserialization marker
|
|
551
|
+
"legacy_deserialization"
|
|
552
|
+
]),
|
|
553
|
+
maxFunctionLength: 1e4,
|
|
554
|
+
// 10KB limit
|
|
555
|
+
enableValidation: true,
|
|
556
|
+
customPatterns: []
|
|
557
|
+
};
|
|
558
|
+
var FunctionValidator = class {
|
|
559
|
+
config;
|
|
560
|
+
allPatterns;
|
|
561
|
+
constructor(config = {}) {
|
|
562
|
+
this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
|
|
563
|
+
this.allPatterns = [
|
|
564
|
+
...BUILTIN_PATTERNS,
|
|
565
|
+
...this.config.customPatterns ?? []
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
addCustomPattern(pattern) {
|
|
569
|
+
this.allPatterns.push(pattern);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Validates function body for security threats.
|
|
573
|
+
*
|
|
574
|
+
* Never throws: returns a structured result.
|
|
575
|
+
*/
|
|
576
|
+
validate(body, functionName) {
|
|
577
|
+
if (!this.config.enableValidation) {
|
|
578
|
+
return {
|
|
579
|
+
isValid: true,
|
|
580
|
+
errors: [],
|
|
581
|
+
riskLevel: "low",
|
|
582
|
+
detectedPatterns: []
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const errors = [];
|
|
586
|
+
const detectedPatterns = [];
|
|
587
|
+
let riskLevel = "low";
|
|
588
|
+
if (body.length > this.config.maxFunctionLength) {
|
|
589
|
+
errors.push(
|
|
590
|
+
`Function body exceeds maximum length of ${this.config.maxFunctionLength} characters`
|
|
591
|
+
);
|
|
592
|
+
riskLevel = "high";
|
|
593
|
+
}
|
|
594
|
+
const riskPriority = {
|
|
595
|
+
low: 0,
|
|
596
|
+
medium: 1,
|
|
597
|
+
high: 2,
|
|
598
|
+
critical: 3
|
|
599
|
+
};
|
|
600
|
+
for (const { pattern, risk, description } of this.allPatterns) {
|
|
601
|
+
if (pattern.global || pattern.sticky) {
|
|
602
|
+
pattern.lastIndex = 0;
|
|
603
|
+
}
|
|
604
|
+
if (pattern.test(body)) {
|
|
605
|
+
errors.push(`Function contains dangerous pattern: ${description}`);
|
|
606
|
+
detectedPatterns.push(description);
|
|
607
|
+
if (riskPriority[risk] > riskPriority[riskLevel]) {
|
|
608
|
+
riskLevel = risk;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (functionName && !this.config.allowedFunctionNames.has(functionName)) {
|
|
613
|
+
errors.push(`Function name '${functionName}' is not in allowed list`);
|
|
614
|
+
if (riskLevel === "low") riskLevel = "medium";
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
isValid: errors.length === 0,
|
|
618
|
+
errors,
|
|
619
|
+
riskLevel,
|
|
620
|
+
detectedPatterns
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Validates function body for security threats
|
|
625
|
+
*/
|
|
626
|
+
validateFunctionBody(body, functionName) {
|
|
627
|
+
const result = this.validate(body, functionName);
|
|
628
|
+
if (!result.isValid) {
|
|
629
|
+
throw new Error(result.errors.join("; "));
|
|
630
|
+
}
|
|
631
|
+
return result.isValid;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Creates a security hash for function validation
|
|
635
|
+
*/
|
|
636
|
+
async createSecurityHash(body, metadata) {
|
|
637
|
+
const encoder = new TextEncoder();
|
|
638
|
+
const data = encoder.encode(body + JSON.stringify(metadata));
|
|
639
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
640
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
641
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
642
|
+
}
|
|
643
|
+
const str = body + JSON.stringify(metadata);
|
|
644
|
+
let hash = 2166136261;
|
|
645
|
+
for (let i = 0; i < str.length; i++) {
|
|
646
|
+
hash ^= str.charCodeAt(i);
|
|
647
|
+
hash = Math.imul(hash, 16777619);
|
|
648
|
+
}
|
|
649
|
+
return (hash >>> 0).toString(16);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Validates security hash
|
|
653
|
+
*/
|
|
654
|
+
async validateSecurityHash(body, metadata, expectedHash) {
|
|
655
|
+
const actualHash = await this.createSecurityHash(body, metadata);
|
|
656
|
+
return actualHash === expectedHash;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
var SafeFunctionSerializer = class {
|
|
660
|
+
validator;
|
|
661
|
+
constructor(config) {
|
|
662
|
+
this.validator = new FunctionValidator(config);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Safely serializes an action with security validation (Async)
|
|
666
|
+
*/
|
|
667
|
+
async serializeActionAsync(action, functionName) {
|
|
668
|
+
if (typeof action === "string") {
|
|
669
|
+
return { type: "string", name: action };
|
|
670
|
+
} else if (typeof action === "function") {
|
|
671
|
+
const body = action.toString();
|
|
672
|
+
this.validator.validateFunctionBody(body, functionName);
|
|
673
|
+
const metadata = {
|
|
674
|
+
length: body.length,
|
|
675
|
+
createdAt: Date.now(),
|
|
676
|
+
...functionName !== void 0 ? { functionName } : {}
|
|
677
|
+
};
|
|
678
|
+
const hash = await this.validator.createSecurityHash(body, metadata);
|
|
679
|
+
return {
|
|
680
|
+
type: "function",
|
|
681
|
+
body,
|
|
682
|
+
hash,
|
|
683
|
+
metadata
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return void 0;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Legacy synchronous serialization (Uses weak hash, kept for backward compatibility)
|
|
690
|
+
* @deprecated Use serializeActionAsync instead for better security
|
|
691
|
+
*/
|
|
692
|
+
serializeAction(action, functionName) {
|
|
693
|
+
if (typeof action === "string") {
|
|
694
|
+
return { type: "string", name: action };
|
|
695
|
+
} else if (typeof action === "function") {
|
|
696
|
+
const body = action.toString();
|
|
697
|
+
this.validator.validateFunctionBody(body, functionName);
|
|
698
|
+
const metadata = {
|
|
699
|
+
length: body.length,
|
|
700
|
+
createdAt: Date.now(),
|
|
701
|
+
...functionName !== void 0 ? { functionName } : {}
|
|
702
|
+
};
|
|
703
|
+
const str = body + JSON.stringify(metadata);
|
|
704
|
+
let hash = 2166136261;
|
|
705
|
+
for (let i = 0; i < str.length; i++) {
|
|
706
|
+
hash ^= str.charCodeAt(i);
|
|
707
|
+
hash = Math.imul(hash, 16777619);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
type: "function",
|
|
711
|
+
body,
|
|
712
|
+
hash: (hash >>> 0).toString(16),
|
|
713
|
+
metadata
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Safely deserializes an action with security validation (Async)
|
|
720
|
+
*/
|
|
721
|
+
async deserializeActionAsync(serializedAction) {
|
|
722
|
+
if (!serializedAction) {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
if (serializedAction.type === "string") {
|
|
726
|
+
return serializedAction.name;
|
|
727
|
+
} else if (serializedAction.type === "function") {
|
|
728
|
+
try {
|
|
729
|
+
const isValid = await this.validator.validateSecurityHash(
|
|
730
|
+
serializedAction.body,
|
|
731
|
+
serializedAction.metadata,
|
|
732
|
+
serializedAction.hash
|
|
733
|
+
);
|
|
734
|
+
if (!isValid) {
|
|
735
|
+
const str = serializedAction.body + JSON.stringify(serializedAction.metadata);
|
|
736
|
+
let hash = 2166136261;
|
|
737
|
+
for (let i = 0; i < str.length; i++) {
|
|
738
|
+
hash ^= str.charCodeAt(i);
|
|
739
|
+
hash = Math.imul(hash, 16777619);
|
|
740
|
+
}
|
|
741
|
+
const legacyHash = (hash >>> 0).toString(16);
|
|
742
|
+
if (legacyHash !== serializedAction.hash) {
|
|
743
|
+
throw new Error("Function security hash validation failed");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
this.validator.validateFunctionBody(
|
|
747
|
+
serializedAction.body,
|
|
748
|
+
serializedAction.metadata.functionName
|
|
749
|
+
);
|
|
750
|
+
return this.createSafeFunction(serializedAction.body);
|
|
751
|
+
} catch (e) {
|
|
752
|
+
securityLogger.error(
|
|
753
|
+
"Error deserializing function",
|
|
754
|
+
{
|
|
755
|
+
functionName: serializedAction.metadata.functionName,
|
|
756
|
+
bodyLength: serializedAction.body.length,
|
|
757
|
+
hash: serializedAction.hash
|
|
758
|
+
},
|
|
759
|
+
e instanceof Error ? e : new Error(String(e))
|
|
760
|
+
);
|
|
761
|
+
return void 0;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Safely deserializes an action (Synchronous - Legacy/Weak)
|
|
768
|
+
*/
|
|
769
|
+
deserializeAction(serializedAction) {
|
|
770
|
+
if (!serializedAction) {
|
|
771
|
+
return void 0;
|
|
772
|
+
}
|
|
773
|
+
if (serializedAction.type === "string") {
|
|
774
|
+
return serializedAction.name;
|
|
775
|
+
} else if (serializedAction.type === "function") {
|
|
776
|
+
try {
|
|
777
|
+
const str = serializedAction.body + JSON.stringify(serializedAction.metadata);
|
|
778
|
+
let hash = 2166136261;
|
|
779
|
+
for (let i = 0; i < str.length; i++) {
|
|
780
|
+
hash ^= str.charCodeAt(i);
|
|
781
|
+
hash = Math.imul(hash, 16777619);
|
|
782
|
+
}
|
|
783
|
+
const calculatedHash = (hash >>> 0).toString(16);
|
|
784
|
+
if (calculatedHash !== serializedAction.hash) {
|
|
785
|
+
if (serializedAction.hash.length === 64) {
|
|
786
|
+
console.warn(
|
|
787
|
+
"Warning: Skipping verification of SHA-256 hash in synchronous deserializeAction. Use deserializeActionAsync for security."
|
|
788
|
+
);
|
|
789
|
+
} else {
|
|
790
|
+
throw new Error("Function security hash validation failed (Legacy)");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
this.validator.validateFunctionBody(
|
|
794
|
+
serializedAction.body,
|
|
795
|
+
serializedAction.metadata.functionName
|
|
796
|
+
);
|
|
797
|
+
return this.createSafeFunction(serializedAction.body);
|
|
798
|
+
} catch (e) {
|
|
799
|
+
securityLogger.error(
|
|
800
|
+
"Error deserializing function",
|
|
801
|
+
{
|
|
802
|
+
functionName: serializedAction.metadata.functionName,
|
|
803
|
+
bodyLength: serializedAction.body.length,
|
|
804
|
+
hash: serializedAction.hash
|
|
805
|
+
},
|
|
806
|
+
e instanceof Error ? e : new Error(String(e))
|
|
807
|
+
);
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return void 0;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Safely deserializes legacy function strings (without security metadata)
|
|
815
|
+
* This method provides backward compatibility for old string-based function serialization
|
|
816
|
+
* while maintaining security through validation and sandboxing.
|
|
817
|
+
*/
|
|
818
|
+
deserializeLegacyString(action) {
|
|
819
|
+
if (!action || typeof action !== "string") {
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
this.validator.validateFunctionBody(action, "legacy_deserialization");
|
|
824
|
+
return this.createSafeFunction(action);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
securityLogger.error(
|
|
827
|
+
"Blocked unsafe legacy function string",
|
|
828
|
+
{ actionLength: action.length },
|
|
829
|
+
e instanceof Error ? e : new Error(String(e))
|
|
830
|
+
);
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Creates a function safely without using new Function()
|
|
836
|
+
* Uses a whitelist approach with predefined function templates
|
|
837
|
+
*/
|
|
838
|
+
createSafeFunction(body) {
|
|
839
|
+
try {
|
|
840
|
+
const source = body.trim();
|
|
841
|
+
const executor = new Function(
|
|
842
|
+
"adaptee",
|
|
843
|
+
"args",
|
|
844
|
+
`
|
|
845
|
+
// Block dangerous globals by creating local variables that shadow them
|
|
846
|
+
var eval = undefined;
|
|
847
|
+
var Function = undefined;
|
|
848
|
+
var setTimeout = undefined;
|
|
849
|
+
var setInterval = undefined;
|
|
850
|
+
var document = undefined;
|
|
851
|
+
var window = undefined;
|
|
852
|
+
var global = undefined;
|
|
853
|
+
var process = undefined;
|
|
854
|
+
var require = undefined;
|
|
855
|
+
|
|
856
|
+
const __fn = (${source});
|
|
857
|
+
if (typeof __fn !== 'function') {
|
|
858
|
+
throw new Error('Deserialized source is not a function');
|
|
859
|
+
}
|
|
860
|
+
return __fn(adaptee, ...(Array.isArray(args) ? args : []));
|
|
861
|
+
`
|
|
862
|
+
);
|
|
863
|
+
return ((adaptee, ...args) => executor(adaptee, args));
|
|
864
|
+
} catch (e) {
|
|
865
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
866
|
+
throw new Error(`Failed to create safe function: ${message}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var safeFunctionSerializer = new SafeFunctionSerializer();
|
|
871
|
+
var serializeAction = safeFunctionSerializer.serializeAction.bind(
|
|
872
|
+
safeFunctionSerializer
|
|
873
|
+
);
|
|
874
|
+
var deserializeAction = safeFunctionSerializer.deserializeAction.bind(
|
|
875
|
+
safeFunctionSerializer
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// src/monitoring.ts
|
|
879
|
+
var HealthStatus = {
|
|
880
|
+
HEALTHY: "healthy",
|
|
881
|
+
WARNING: "warning",
|
|
882
|
+
CRITICAL: "critical",
|
|
883
|
+
UNKNOWN: "unknown"
|
|
884
|
+
};
|
|
885
|
+
var DEFAULT_MONITORING_CONFIG = {
|
|
886
|
+
enabled: true,
|
|
887
|
+
metricsInterval: 5e3,
|
|
888
|
+
// 5 seconds
|
|
889
|
+
healthCheckInterval: 3e4,
|
|
890
|
+
// 30 seconds
|
|
891
|
+
maxMetricsHistory: 1e3,
|
|
892
|
+
thresholds: {
|
|
893
|
+
transitionTimeWarning: 100,
|
|
894
|
+
// 100ms
|
|
895
|
+
transitionTimeCritical: 500,
|
|
896
|
+
// 500ms
|
|
897
|
+
errorRateWarning: 5,
|
|
898
|
+
// 5%
|
|
899
|
+
errorRateCritical: 15
|
|
900
|
+
// 15%
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
var MetricsCollector = class {
|
|
904
|
+
metrics = [];
|
|
905
|
+
config;
|
|
906
|
+
startTime = Date.now();
|
|
907
|
+
transitionCount = 0;
|
|
908
|
+
errorCount = 0;
|
|
909
|
+
constructor(config = {}) {
|
|
910
|
+
this.config = { ...DEFAULT_MONITORING_CONFIG, ...config };
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Record a state transition
|
|
914
|
+
*/
|
|
915
|
+
recordTransition(transitionTime) {
|
|
916
|
+
if (!this.config.enabled) return;
|
|
917
|
+
this.transitionCount++;
|
|
918
|
+
const metrics = {
|
|
919
|
+
transitionTime,
|
|
920
|
+
stateChangeCount: this.transitionCount,
|
|
921
|
+
errorCount: this.errorCount,
|
|
922
|
+
timestamp: Date.now()
|
|
923
|
+
};
|
|
924
|
+
this.addMetrics(metrics);
|
|
925
|
+
this.logMetrics(metrics);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Record an error
|
|
929
|
+
*/
|
|
930
|
+
recordError() {
|
|
931
|
+
if (!this.config.enabled) return;
|
|
932
|
+
this.errorCount++;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Get current metrics summary
|
|
936
|
+
*/
|
|
937
|
+
getMetricsSummary() {
|
|
938
|
+
const now = Date.now();
|
|
939
|
+
const uptime = now - this.startTime;
|
|
940
|
+
const recentMetrics = this.getRecentMetrics(6e4);
|
|
941
|
+
const averageTransitionTime = recentMetrics.length > 0 ? recentMetrics.reduce((sum, m) => sum + m.transitionTime, 0) / recentMetrics.length : 0;
|
|
942
|
+
const errorRate = this.transitionCount > 0 ? this.errorCount / this.transitionCount * 100 : 0;
|
|
943
|
+
return {
|
|
944
|
+
totalTransitions: this.transitionCount,
|
|
945
|
+
totalErrors: this.errorCount,
|
|
946
|
+
averageTransitionTime,
|
|
947
|
+
errorRate,
|
|
948
|
+
uptime
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Get metrics history
|
|
953
|
+
*/
|
|
954
|
+
getMetricsHistory(timeRange) {
|
|
955
|
+
if (timeRange) {
|
|
956
|
+
return this.getRecentMetrics(timeRange);
|
|
957
|
+
}
|
|
958
|
+
return [...this.metrics];
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Clear metrics history
|
|
962
|
+
*/
|
|
963
|
+
clearMetrics() {
|
|
964
|
+
this.metrics = [];
|
|
965
|
+
this.transitionCount = 0;
|
|
966
|
+
this.errorCount = 0;
|
|
967
|
+
this.startTime = Date.now();
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Get recent metrics within time range
|
|
971
|
+
*/
|
|
972
|
+
getRecentMetrics(timeRange) {
|
|
973
|
+
const cutoff = Date.now() - timeRange;
|
|
974
|
+
return this.metrics.filter((m) => m.timestamp >= cutoff);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Add metrics to history with size limit
|
|
978
|
+
*/
|
|
979
|
+
addMetrics(metrics) {
|
|
980
|
+
this.metrics.push(metrics);
|
|
981
|
+
if (this.metrics.length > this.config.maxMetricsHistory) {
|
|
982
|
+
this.metrics = this.metrics.slice(-this.config.maxMetricsHistory);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Log metrics if significant
|
|
987
|
+
*/
|
|
988
|
+
logMetrics(metrics) {
|
|
989
|
+
const { thresholds } = this.config;
|
|
990
|
+
if (metrics.transitionTime > thresholds.transitionTimeCritical) {
|
|
991
|
+
stateMachineLogger.error("Critical transition time detected", {
|
|
992
|
+
transitionTime: metrics.transitionTime,
|
|
993
|
+
threshold: thresholds.transitionTimeCritical
|
|
994
|
+
});
|
|
995
|
+
} else if (metrics.transitionTime > thresholds.transitionTimeWarning) {
|
|
996
|
+
stateMachineLogger.warn("Slow transition detected", {
|
|
997
|
+
transitionTime: metrics.transitionTime,
|
|
998
|
+
threshold: thresholds.transitionTimeWarning
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var PerformanceMonitor = class {
|
|
1004
|
+
metricsCollector;
|
|
1005
|
+
config;
|
|
1006
|
+
intervalId;
|
|
1007
|
+
isRunning = false;
|
|
1008
|
+
constructor(metricsCollector, config = {}) {
|
|
1009
|
+
this.metricsCollector = metricsCollector;
|
|
1010
|
+
this.config = { ...DEFAULT_MONITORING_CONFIG, ...config };
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Start performance monitoring
|
|
1014
|
+
*/
|
|
1015
|
+
start() {
|
|
1016
|
+
if (this.isRunning || !this.config.enabled) return;
|
|
1017
|
+
this.isRunning = true;
|
|
1018
|
+
this.intervalId = setInterval(() => {
|
|
1019
|
+
this.collectMetrics();
|
|
1020
|
+
}, this.config.metricsInterval);
|
|
1021
|
+
stateMachineLogger.info("Performance monitoring started", {
|
|
1022
|
+
interval: this.config.metricsInterval
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Stop performance monitoring
|
|
1027
|
+
*/
|
|
1028
|
+
stop() {
|
|
1029
|
+
if (!this.isRunning) return;
|
|
1030
|
+
this.isRunning = false;
|
|
1031
|
+
if (this.intervalId) {
|
|
1032
|
+
clearInterval(this.intervalId);
|
|
1033
|
+
this.intervalId = void 0;
|
|
1034
|
+
}
|
|
1035
|
+
stateMachineLogger.info("Performance monitoring stopped");
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get current performance status
|
|
1039
|
+
*/
|
|
1040
|
+
getPerformanceStatus() {
|
|
1041
|
+
const summary = this.metricsCollector.getMetricsSummary();
|
|
1042
|
+
const issues = [];
|
|
1043
|
+
let status = HealthStatus.HEALTHY;
|
|
1044
|
+
if (summary.errorRate >= this.config.thresholds.errorRateCritical) {
|
|
1045
|
+
status = HealthStatus.CRITICAL;
|
|
1046
|
+
issues.push(`Critical error rate: ${summary.errorRate.toFixed(2)}%`);
|
|
1047
|
+
} else if (summary.errorRate >= this.config.thresholds.errorRateWarning) {
|
|
1048
|
+
status = HealthStatus.WARNING;
|
|
1049
|
+
issues.push(`High error rate: ${summary.errorRate.toFixed(2)}%`);
|
|
1050
|
+
}
|
|
1051
|
+
if (summary.averageTransitionTime >= this.config.thresholds.transitionTimeCritical) {
|
|
1052
|
+
status = HealthStatus.CRITICAL;
|
|
1053
|
+
issues.push(
|
|
1054
|
+
`Critical transition time: ${summary.averageTransitionTime.toFixed(2)}ms`
|
|
1055
|
+
);
|
|
1056
|
+
} else if (summary.averageTransitionTime >= this.config.thresholds.transitionTimeWarning) {
|
|
1057
|
+
if (status === HealthStatus.HEALTHY) status = HealthStatus.WARNING;
|
|
1058
|
+
issues.push(
|
|
1059
|
+
`Slow transitions: ${summary.averageTransitionTime.toFixed(2)}ms`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
return { status, summary, issues };
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Collect current metrics
|
|
1066
|
+
*/
|
|
1067
|
+
collectMetrics() {
|
|
1068
|
+
const summary = this.metricsCollector.getMetricsSummary();
|
|
1069
|
+
stateMachineLogger.debug("Performance metrics collected", {
|
|
1070
|
+
totalTransitions: summary.totalTransitions,
|
|
1071
|
+
errorRate: summary.errorRate,
|
|
1072
|
+
averageTransitionTime: summary.averageTransitionTime
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
var HealthChecker = class {
|
|
1077
|
+
performanceMonitor;
|
|
1078
|
+
config;
|
|
1079
|
+
intervalId;
|
|
1080
|
+
isRunning = false;
|
|
1081
|
+
lastHealthCheck;
|
|
1082
|
+
constructor(performanceMonitor, config = {}) {
|
|
1083
|
+
this.performanceMonitor = performanceMonitor;
|
|
1084
|
+
this.config = { ...DEFAULT_MONITORING_CONFIG, ...config };
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Start health checking
|
|
1088
|
+
*/
|
|
1089
|
+
start() {
|
|
1090
|
+
if (this.isRunning || !this.config.enabled) return;
|
|
1091
|
+
this.isRunning = true;
|
|
1092
|
+
this.intervalId = setInterval(() => {
|
|
1093
|
+
this.performHealthCheck();
|
|
1094
|
+
}, this.config.healthCheckInterval);
|
|
1095
|
+
stateMachineLogger.info("Health checking started", {
|
|
1096
|
+
interval: this.config.healthCheckInterval
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Stop health checking
|
|
1101
|
+
*/
|
|
1102
|
+
stop() {
|
|
1103
|
+
if (!this.isRunning) return;
|
|
1104
|
+
this.isRunning = false;
|
|
1105
|
+
if (this.intervalId) {
|
|
1106
|
+
clearInterval(this.intervalId);
|
|
1107
|
+
this.intervalId = void 0;
|
|
1108
|
+
}
|
|
1109
|
+
stateMachineLogger.info("Health checking stopped");
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Perform immediate health check
|
|
1113
|
+
*/
|
|
1114
|
+
performHealthCheck() {
|
|
1115
|
+
const performanceStatus = this.performanceMonitor.getPerformanceStatus();
|
|
1116
|
+
const result = {
|
|
1117
|
+
status: performanceStatus.status,
|
|
1118
|
+
message: this.generateHealthMessage(performanceStatus),
|
|
1119
|
+
timestamp: Date.now(),
|
|
1120
|
+
metrics: performanceStatus.summary
|
|
1121
|
+
};
|
|
1122
|
+
this.lastHealthCheck = result;
|
|
1123
|
+
this.logHealthCheck(result);
|
|
1124
|
+
return result;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Get last health check result
|
|
1128
|
+
*/
|
|
1129
|
+
getLastHealthCheck() {
|
|
1130
|
+
return this.lastHealthCheck;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Generate health message based on status
|
|
1134
|
+
*/
|
|
1135
|
+
generateHealthMessage(performanceStatus) {
|
|
1136
|
+
const { status, issues, summary } = performanceStatus;
|
|
1137
|
+
if (status === HealthStatus.HEALTHY) {
|
|
1138
|
+
return `StateMachine is healthy. ${summary.totalTransitions} transitions, ${summary.errorRate.toFixed(2)}% error rate`;
|
|
1139
|
+
}
|
|
1140
|
+
if (status === HealthStatus.WARNING) {
|
|
1141
|
+
return `StateMachine has warnings: ${issues.join(", ")}`;
|
|
1142
|
+
}
|
|
1143
|
+
if (status === HealthStatus.CRITICAL) {
|
|
1144
|
+
return `StateMachine is in critical state: ${issues.join(", ")}`;
|
|
1145
|
+
}
|
|
1146
|
+
return "StateMachine health status unknown";
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Log health check results
|
|
1150
|
+
*/
|
|
1151
|
+
logHealthCheck(result) {
|
|
1152
|
+
const logData = {
|
|
1153
|
+
status: result.status,
|
|
1154
|
+
message: result.message,
|
|
1155
|
+
metrics: result.metrics
|
|
1156
|
+
};
|
|
1157
|
+
switch (result.status) {
|
|
1158
|
+
case HealthStatus.HEALTHY:
|
|
1159
|
+
stateMachineLogger.debug("Health check passed", logData);
|
|
1160
|
+
break;
|
|
1161
|
+
case HealthStatus.WARNING:
|
|
1162
|
+
stateMachineLogger.warn("Health check warning", logData);
|
|
1163
|
+
break;
|
|
1164
|
+
case HealthStatus.CRITICAL:
|
|
1165
|
+
stateMachineLogger.error("Health check critical", logData);
|
|
1166
|
+
break;
|
|
1167
|
+
/* c8 ignore next 2 */
|
|
1168
|
+
default:
|
|
1169
|
+
stateMachineLogger.info("Health check completed", logData);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
var StateMachineMonitor = class {
|
|
1174
|
+
metricsCollector;
|
|
1175
|
+
performanceMonitor;
|
|
1176
|
+
healthChecker;
|
|
1177
|
+
config;
|
|
1178
|
+
constructor(config = {}) {
|
|
1179
|
+
this.config = { ...DEFAULT_MONITORING_CONFIG, ...config };
|
|
1180
|
+
this.metricsCollector = new MetricsCollector(this.config);
|
|
1181
|
+
this.performanceMonitor = new PerformanceMonitor(
|
|
1182
|
+
this.metricsCollector,
|
|
1183
|
+
this.config
|
|
1184
|
+
);
|
|
1185
|
+
this.healthChecker = new HealthChecker(this.performanceMonitor, this.config);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Start all monitoring components
|
|
1189
|
+
*/
|
|
1190
|
+
start() {
|
|
1191
|
+
this.performanceMonitor.start();
|
|
1192
|
+
this.healthChecker.start();
|
|
1193
|
+
stateMachineLogger.info("StateMachine monitoring started", {
|
|
1194
|
+
config: this.config
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Stop all monitoring components
|
|
1199
|
+
*/
|
|
1200
|
+
stop() {
|
|
1201
|
+
this.performanceMonitor.stop();
|
|
1202
|
+
this.healthChecker.stop();
|
|
1203
|
+
stateMachineLogger.info("StateMachine monitoring stopped");
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Record a transition for monitoring (F-T4-TS-5 widening: added success + context params)
|
|
1207
|
+
*/
|
|
1208
|
+
recordTransition(transitionTime, success = true, _context) {
|
|
1209
|
+
this.metricsCollector.recordTransition(transitionTime);
|
|
1210
|
+
if (!success) this.metricsCollector.recordError();
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Record an error for monitoring (F-T4-TS-5 widening: added error + context params)
|
|
1214
|
+
*/
|
|
1215
|
+
recordError(_error, _context) {
|
|
1216
|
+
this.metricsCollector.recordError();
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Record an event for monitoring (optional IMonitor extension)
|
|
1220
|
+
*/
|
|
1221
|
+
recordEvent(_eventName, _duration) {
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get aggregate metrics snapshot (optional IMonitor extension)
|
|
1225
|
+
*/
|
|
1226
|
+
getMetrics() {
|
|
1227
|
+
const summary = this.metricsCollector.getMetricsSummary();
|
|
1228
|
+
return {
|
|
1229
|
+
totalTransitions: summary.totalTransitions,
|
|
1230
|
+
successCount: summary.totalTransitions - summary.totalErrors,
|
|
1231
|
+
errorCount: summary.totalErrors,
|
|
1232
|
+
averageDuration: summary.averageTransitionTime
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Get comprehensive monitoring report
|
|
1237
|
+
*/
|
|
1238
|
+
getMonitoringReport() {
|
|
1239
|
+
const health = this.healthChecker.performHealthCheck();
|
|
1240
|
+
const performance = this.performanceMonitor.getPerformanceStatus();
|
|
1241
|
+
const metrics = this.metricsCollector.getMetricsSummary();
|
|
1242
|
+
return {
|
|
1243
|
+
health,
|
|
1244
|
+
performance,
|
|
1245
|
+
metrics,
|
|
1246
|
+
config: this.config
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Export metrics for external monitoring systems
|
|
1251
|
+
*/
|
|
1252
|
+
exportMetrics() {
|
|
1253
|
+
const metrics = this.metricsCollector.getMetricsSummary();
|
|
1254
|
+
const health = this.healthChecker.getLastHealthCheck();
|
|
1255
|
+
const jsonExport = {
|
|
1256
|
+
timestamp: Date.now(),
|
|
1257
|
+
metrics,
|
|
1258
|
+
health,
|
|
1259
|
+
config: this.config
|
|
1260
|
+
};
|
|
1261
|
+
const prometheusExport = this.generatePrometheusMetrics(metrics, health);
|
|
1262
|
+
return {
|
|
1263
|
+
prometheus: prometheusExport,
|
|
1264
|
+
json: jsonExport
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Generate Prometheus-compatible metrics (without memory metrics)
|
|
1269
|
+
*/
|
|
1270
|
+
generatePrometheusMetrics(metrics, health) {
|
|
1271
|
+
const lines = [];
|
|
1272
|
+
lines.push(
|
|
1273
|
+
"# HELP statemachine_transitions_total Total number of state transitions"
|
|
1274
|
+
);
|
|
1275
|
+
lines.push("# TYPE statemachine_transitions_total counter");
|
|
1276
|
+
lines.push(`statemachine_transitions_total ${metrics.totalTransitions}`);
|
|
1277
|
+
lines.push("# HELP statemachine_errors_total Total number of errors");
|
|
1278
|
+
lines.push("# TYPE statemachine_errors_total counter");
|
|
1279
|
+
lines.push(`statemachine_errors_total ${metrics.totalErrors}`);
|
|
1280
|
+
lines.push(
|
|
1281
|
+
"# HELP statemachine_transition_time_avg Average transition time in milliseconds"
|
|
1282
|
+
);
|
|
1283
|
+
lines.push("# TYPE statemachine_transition_time_avg gauge");
|
|
1284
|
+
lines.push(
|
|
1285
|
+
`statemachine_transition_time_avg ${metrics.averageTransitionTime}`
|
|
1286
|
+
);
|
|
1287
|
+
lines.push("# HELP statemachine_error_rate Error rate percentage");
|
|
1288
|
+
lines.push("# TYPE statemachine_error_rate gauge");
|
|
1289
|
+
lines.push(`statemachine_error_rate ${metrics.errorRate}`);
|
|
1290
|
+
lines.push("# HELP statemachine_uptime Uptime in milliseconds");
|
|
1291
|
+
lines.push("# TYPE statemachine_uptime gauge");
|
|
1292
|
+
lines.push(`statemachine_uptime ${metrics.uptime}`);
|
|
1293
|
+
if (health) {
|
|
1294
|
+
const healthValue = health.status === HealthStatus.HEALTHY ? 1 : health.status === HealthStatus.WARNING ? 0.5 : 0;
|
|
1295
|
+
lines.push(
|
|
1296
|
+
"# HELP statemachine_health Health status (1=healthy, 0.5=warning, 0=critical)"
|
|
1297
|
+
);
|
|
1298
|
+
lines.push("# TYPE statemachine_health gauge");
|
|
1299
|
+
lines.push(`statemachine_health ${healthValue}`);
|
|
1300
|
+
}
|
|
1301
|
+
return lines.join("\n");
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
function createDefaultMonitor() {
|
|
1305
|
+
return new StateMachineMonitor();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/types.ts
|
|
1309
|
+
var StateMachineError = class _StateMachineError extends Error {
|
|
1310
|
+
context;
|
|
1311
|
+
constructor(message, context, cause) {
|
|
1312
|
+
super(message);
|
|
1313
|
+
this.name = "StateMachineError";
|
|
1314
|
+
this.context = context;
|
|
1315
|
+
this.cause = cause;
|
|
1316
|
+
if (Error.captureStackTrace) {
|
|
1317
|
+
Error.captureStackTrace(this, _StateMachineError);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
toString() {
|
|
1321
|
+
const { state, event, action, transition, phase } = this.context;
|
|
1322
|
+
const details = [];
|
|
1323
|
+
if (state) details.push(`state: ${state}`);
|
|
1324
|
+
if (event) details.push(`event: ${event}`);
|
|
1325
|
+
if (action) details.push(`action: ${action}`);
|
|
1326
|
+
if (transition) details.push(`transition: ${transition}`);
|
|
1327
|
+
if (phase) details.push(`phase: ${phase}`);
|
|
1328
|
+
return `${this.name}: ${this.message}${details.length ? ` (${details.join(", ")})` : ""}`;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
function isAdapter(inp) {
|
|
1332
|
+
return !!inp && typeof inp === "object" && "set" in inp && "get" in inp;
|
|
1333
|
+
}
|
|
1334
|
+
var MemoryAdapter = class {
|
|
1335
|
+
adaptee;
|
|
1336
|
+
_savedState;
|
|
1337
|
+
constructor(data) {
|
|
1338
|
+
this.adaptee = data;
|
|
1339
|
+
}
|
|
1340
|
+
set(property, value) {
|
|
1341
|
+
this.adaptee[property] = value;
|
|
1342
|
+
}
|
|
1343
|
+
get(property) {
|
|
1344
|
+
return this.adaptee[property];
|
|
1345
|
+
}
|
|
1346
|
+
// StatePersistenceAdapter implementation
|
|
1347
|
+
async save(state) {
|
|
1348
|
+
this._savedState = state;
|
|
1349
|
+
}
|
|
1350
|
+
async restore() {
|
|
1351
|
+
return this._savedState;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
var LocalStorageAdapter = class {
|
|
1355
|
+
adaptee;
|
|
1356
|
+
key;
|
|
1357
|
+
constructor(data, storageKey = "state_machine") {
|
|
1358
|
+
this.adaptee = data;
|
|
1359
|
+
this.key = storageKey;
|
|
1360
|
+
}
|
|
1361
|
+
set(property, value) {
|
|
1362
|
+
this.adaptee[property] = value;
|
|
1363
|
+
}
|
|
1364
|
+
get(property) {
|
|
1365
|
+
return this.adaptee[property];
|
|
1366
|
+
}
|
|
1367
|
+
// StatePersistenceAdapter implementation
|
|
1368
|
+
async save(state) {
|
|
1369
|
+
const data = { ...this.adaptee, state };
|
|
1370
|
+
globalThis.localStorage.setItem(this.key, JSON.stringify(data));
|
|
1371
|
+
}
|
|
1372
|
+
async restore() {
|
|
1373
|
+
const stored = globalThis.localStorage.getItem(this.key);
|
|
1374
|
+
return stored ? JSON.parse(stored).state : { currentState: "", history: {} };
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
var SessionStorageAdapter = class {
|
|
1378
|
+
adaptee;
|
|
1379
|
+
key;
|
|
1380
|
+
constructor(data, storageKey = "state_machine") {
|
|
1381
|
+
this.adaptee = data;
|
|
1382
|
+
this.key = storageKey;
|
|
1383
|
+
}
|
|
1384
|
+
set(property, value) {
|
|
1385
|
+
this.adaptee[property] = value;
|
|
1386
|
+
}
|
|
1387
|
+
get(property) {
|
|
1388
|
+
return this.adaptee[property];
|
|
1389
|
+
}
|
|
1390
|
+
// StatePersistenceAdapter implementation
|
|
1391
|
+
async save(state) {
|
|
1392
|
+
const data = { ...this.adaptee, state };
|
|
1393
|
+
globalThis.sessionStorage.setItem(this.key, JSON.stringify(data));
|
|
1394
|
+
}
|
|
1395
|
+
async restore() {
|
|
1396
|
+
const stored = globalThis.sessionStorage.getItem(this.key);
|
|
1397
|
+
return stored ? JSON.parse(stored).state : void 0;
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
var ServerAdapter = class _ServerAdapter {
|
|
1401
|
+
adaptee;
|
|
1402
|
+
static data = {};
|
|
1403
|
+
endpoint;
|
|
1404
|
+
constructor(data, endpoint = "/api/state") {
|
|
1405
|
+
this.adaptee = data;
|
|
1406
|
+
this.endpoint = endpoint;
|
|
1407
|
+
}
|
|
1408
|
+
set(property, value) {
|
|
1409
|
+
this.adaptee[property] = value;
|
|
1410
|
+
}
|
|
1411
|
+
get(property) {
|
|
1412
|
+
return this.adaptee[property];
|
|
1413
|
+
}
|
|
1414
|
+
// StatePersistenceAdapter implementation (simulated async)
|
|
1415
|
+
async save(state) {
|
|
1416
|
+
if (state) {
|
|
1417
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1418
|
+
_ServerAdapter.data[this.endpoint] = state;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async restore() {
|
|
1422
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1423
|
+
return _ServerAdapter.data[this.endpoint] || { currentState: "", history: {} };
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// src/error_handling.ts
|
|
1428
|
+
var ErrorSeverity = {
|
|
1429
|
+
LOW: "low",
|
|
1430
|
+
MEDIUM: "medium",
|
|
1431
|
+
HIGH: "high",
|
|
1432
|
+
CRITICAL: "critical"
|
|
1433
|
+
};
|
|
1434
|
+
var ErrorCategory = {
|
|
1435
|
+
VALIDATION: "validation",
|
|
1436
|
+
TRANSITION: "transition",
|
|
1437
|
+
ACTION: "action",
|
|
1438
|
+
SERIALIZATION: "serialization",
|
|
1439
|
+
CONFIGURATION: "configuration",
|
|
1440
|
+
SECURITY: "security",
|
|
1441
|
+
PERFORMANCE: "performance",
|
|
1442
|
+
NETWORK: "network",
|
|
1443
|
+
UNKNOWN: "unknown"
|
|
1444
|
+
};
|
|
1445
|
+
var EnhancedStateMachineError = class extends StateMachineError {
|
|
1446
|
+
severity;
|
|
1447
|
+
category;
|
|
1448
|
+
extendedContext;
|
|
1449
|
+
recoverable;
|
|
1450
|
+
errorCode;
|
|
1451
|
+
constructor(message, context, options = {}) {
|
|
1452
|
+
super(message, context, options.cause);
|
|
1453
|
+
this.severity = options.severity || ErrorSeverity.MEDIUM;
|
|
1454
|
+
this.category = options.category || ErrorCategory.UNKNOWN;
|
|
1455
|
+
this.recoverable = options.recoverable ?? true;
|
|
1456
|
+
this.errorCode = options.errorCode || this.generateErrorCode();
|
|
1457
|
+
this.extendedContext = {
|
|
1458
|
+
...context,
|
|
1459
|
+
timestamp: Date.now(),
|
|
1460
|
+
stackTrace: this.stack,
|
|
1461
|
+
additionalData: options.additionalData
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
generateErrorCode() {
|
|
1465
|
+
const categoryCode = this.category.toUpperCase().substring(0, 3);
|
|
1466
|
+
const severityCode = this.severity.toUpperCase().substring(0, 1);
|
|
1467
|
+
const timestamp = Date.now().toString().slice(-6);
|
|
1468
|
+
return `SM_${categoryCode}_${severityCode}_${timestamp}`;
|
|
1469
|
+
}
|
|
1470
|
+
toJSON() {
|
|
1471
|
+
return {
|
|
1472
|
+
name: this.name,
|
|
1473
|
+
message: this.message,
|
|
1474
|
+
errorCode: this.errorCode,
|
|
1475
|
+
severity: this.severity,
|
|
1476
|
+
category: this.category,
|
|
1477
|
+
recoverable: this.recoverable,
|
|
1478
|
+
context: this.extendedContext,
|
|
1479
|
+
stack: this.stack
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
var RetryRecoveryStrategy = class {
|
|
1484
|
+
name = "retry";
|
|
1485
|
+
maxRetries;
|
|
1486
|
+
retryDelay;
|
|
1487
|
+
constructor(maxRetries = 3, retryDelay = 1e3) {
|
|
1488
|
+
this.maxRetries = maxRetries;
|
|
1489
|
+
this.retryDelay = retryDelay;
|
|
1490
|
+
}
|
|
1491
|
+
canRecover(error) {
|
|
1492
|
+
return error.recoverable && error.category !== ErrorCategory.SECURITY && error.severity !== ErrorSeverity.CRITICAL;
|
|
1493
|
+
}
|
|
1494
|
+
async recover(error, context) {
|
|
1495
|
+
const retryCount = context.retryCount || 0;
|
|
1496
|
+
if (retryCount >= this.maxRetries) {
|
|
1497
|
+
stateMachineLogger.warn("Max retries exceeded", {
|
|
1498
|
+
errorCode: error.errorCode,
|
|
1499
|
+
retryCount,
|
|
1500
|
+
maxRetries: this.maxRetries
|
|
1501
|
+
});
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
stateMachineLogger.info("Attempting error recovery", {
|
|
1505
|
+
errorCode: error.errorCode,
|
|
1506
|
+
strategy: this.name,
|
|
1507
|
+
attempt: retryCount + 1
|
|
1508
|
+
});
|
|
1509
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelay));
|
|
1510
|
+
context.retryCount = retryCount + 1;
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
var FallbackStateRecoveryStrategy = class {
|
|
1515
|
+
name = "fallback_state";
|
|
1516
|
+
fallbackState;
|
|
1517
|
+
constructor(fallbackState = "error_state") {
|
|
1518
|
+
this.fallbackState = fallbackState;
|
|
1519
|
+
}
|
|
1520
|
+
canRecover(error) {
|
|
1521
|
+
return error.category === ErrorCategory.TRANSITION || error.category === ErrorCategory.ACTION;
|
|
1522
|
+
}
|
|
1523
|
+
async recover(error, context) {
|
|
1524
|
+
try {
|
|
1525
|
+
if (context.stateMachine && typeof context.stateMachine.setCurrentState === "function") {
|
|
1526
|
+
stateMachineLogger.info("Recovering to fallback state", {
|
|
1527
|
+
errorCode: error.errorCode,
|
|
1528
|
+
fallbackState: this.fallbackState,
|
|
1529
|
+
originalState: error.context.state
|
|
1530
|
+
});
|
|
1531
|
+
context.stateMachine.setCurrentState(this.fallbackState);
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
} catch (recoveryError) {
|
|
1535
|
+
stateMachineLogger.error(
|
|
1536
|
+
"Fallback recovery failed",
|
|
1537
|
+
{
|
|
1538
|
+
errorCode: error.errorCode,
|
|
1539
|
+
fallbackState: this.fallbackState
|
|
1540
|
+
},
|
|
1541
|
+
/* c8 ignore next */
|
|
1542
|
+
recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError))
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
var ErrorAnalytics = class {
|
|
1549
|
+
errors = [];
|
|
1550
|
+
maxStoredErrors;
|
|
1551
|
+
constructor(maxStoredErrors = 1e3) {
|
|
1552
|
+
this.maxStoredErrors = maxStoredErrors;
|
|
1553
|
+
}
|
|
1554
|
+
recordError(error) {
|
|
1555
|
+
this.errors.push(error);
|
|
1556
|
+
if (this.errors.length > this.maxStoredErrors) {
|
|
1557
|
+
this.errors = this.errors.slice(-this.maxStoredErrors);
|
|
1558
|
+
}
|
|
1559
|
+
stateMachineLogger.error(
|
|
1560
|
+
"StateMachine error recorded",
|
|
1561
|
+
{
|
|
1562
|
+
errorCode: error.errorCode,
|
|
1563
|
+
severity: error.severity,
|
|
1564
|
+
category: error.category,
|
|
1565
|
+
recoverable: error.recoverable
|
|
1566
|
+
},
|
|
1567
|
+
error
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
getErrorStats() {
|
|
1571
|
+
const now = Date.now();
|
|
1572
|
+
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
1573
|
+
const recentErrors = this.errors.filter(
|
|
1574
|
+
(e) => e.extendedContext.timestamp > oneHourAgo
|
|
1575
|
+
);
|
|
1576
|
+
const bySeverity = Object.values(ErrorSeverity).reduce(
|
|
1577
|
+
(acc, severity) => {
|
|
1578
|
+
acc[severity] = this.errors.filter(
|
|
1579
|
+
(e) => e.severity === severity
|
|
1580
|
+
).length;
|
|
1581
|
+
return acc;
|
|
1582
|
+
},
|
|
1583
|
+
{}
|
|
1584
|
+
);
|
|
1585
|
+
const byCategory = Object.values(ErrorCategory).reduce(
|
|
1586
|
+
(acc, category) => {
|
|
1587
|
+
acc[category] = this.errors.filter(
|
|
1588
|
+
(e) => e.category === category
|
|
1589
|
+
).length;
|
|
1590
|
+
return acc;
|
|
1591
|
+
},
|
|
1592
|
+
{}
|
|
1593
|
+
);
|
|
1594
|
+
return {
|
|
1595
|
+
total: this.errors.length,
|
|
1596
|
+
bySeverity,
|
|
1597
|
+
byCategory,
|
|
1598
|
+
recentErrors: recentErrors.length,
|
|
1599
|
+
errorRate: recentErrors.length / 60
|
|
1600
|
+
// errors per minute
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
getTopErrors(limit = 10) {
|
|
1604
|
+
const errorGroups = /* @__PURE__ */ new Map();
|
|
1605
|
+
this.errors.forEach((error) => {
|
|
1606
|
+
const key = `${error.message}_${error.category}`;
|
|
1607
|
+
const existing = errorGroups.get(key);
|
|
1608
|
+
if (existing) {
|
|
1609
|
+
existing.count++;
|
|
1610
|
+
existing.lastOccurrence = Math.max(
|
|
1611
|
+
existing.lastOccurrence,
|
|
1612
|
+
error.extendedContext.timestamp
|
|
1613
|
+
);
|
|
1614
|
+
} else {
|
|
1615
|
+
errorGroups.set(key, {
|
|
1616
|
+
errorCode: error.errorCode,
|
|
1617
|
+
message: error.message,
|
|
1618
|
+
count: 1,
|
|
1619
|
+
lastOccurrence: error.extendedContext.timestamp,
|
|
1620
|
+
severity: error.severity,
|
|
1621
|
+
category: error.category
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
return Array.from(errorGroups.values()).sort((a, b) => b.count - a.count).slice(0, limit);
|
|
1626
|
+
}
|
|
1627
|
+
clearErrors() {
|
|
1628
|
+
this.errors = [];
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
var ErrorHandler = class {
|
|
1632
|
+
recoveryStrategies = [];
|
|
1633
|
+
analytics;
|
|
1634
|
+
enabled = true;
|
|
1635
|
+
constructor() {
|
|
1636
|
+
this.analytics = new ErrorAnalytics();
|
|
1637
|
+
this.addRecoveryStrategy(new RetryRecoveryStrategy());
|
|
1638
|
+
this.addRecoveryStrategy(new FallbackStateRecoveryStrategy());
|
|
1639
|
+
}
|
|
1640
|
+
addRecoveryStrategy(strategy) {
|
|
1641
|
+
this.recoveryStrategies.push(strategy);
|
|
1642
|
+
}
|
|
1643
|
+
removeRecoveryStrategy(strategyName) {
|
|
1644
|
+
this.recoveryStrategies = this.recoveryStrategies.filter(
|
|
1645
|
+
(s) => s.name !== strategyName
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
async handleError(error, context = {}) {
|
|
1649
|
+
if (!this.enabled) {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
const enhancedError = error instanceof EnhancedStateMachineError ? error : this.convertToEnhancedError(error, context);
|
|
1653
|
+
this.analytics.recordError(enhancedError);
|
|
1654
|
+
for (const strategy of this.recoveryStrategies) {
|
|
1655
|
+
if (strategy.canRecover(enhancedError)) {
|
|
1656
|
+
try {
|
|
1657
|
+
const recovered = await strategy.recover(enhancedError, context);
|
|
1658
|
+
if (recovered) {
|
|
1659
|
+
stateMachineLogger.info("Error recovery successful", {
|
|
1660
|
+
errorCode: enhancedError.errorCode,
|
|
1661
|
+
strategy: strategy.name
|
|
1662
|
+
});
|
|
1663
|
+
return true;
|
|
1664
|
+
}
|
|
1665
|
+
} catch (recoveryError) {
|
|
1666
|
+
stateMachineLogger.error(
|
|
1667
|
+
"Recovery strategy failed",
|
|
1668
|
+
{
|
|
1669
|
+
errorCode: enhancedError.errorCode,
|
|
1670
|
+
strategy: strategy.name
|
|
1671
|
+
},
|
|
1672
|
+
/* c8 ignore next */
|
|
1673
|
+
recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError))
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
stateMachineLogger.error("All recovery strategies failed", {
|
|
1679
|
+
errorCode: enhancedError.errorCode,
|
|
1680
|
+
severity: enhancedError.severity,
|
|
1681
|
+
category: enhancedError.category
|
|
1682
|
+
});
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
convertToEnhancedError(error, context) {
|
|
1686
|
+
let category = ErrorCategory.UNKNOWN;
|
|
1687
|
+
let severity = ErrorSeverity.MEDIUM;
|
|
1688
|
+
if (error.message.includes("transition") || error.message.includes("state")) {
|
|
1689
|
+
category = ErrorCategory.TRANSITION;
|
|
1690
|
+
} else if (error.message.includes("action")) {
|
|
1691
|
+
category = ErrorCategory.ACTION;
|
|
1692
|
+
} else if (error.message.includes("validation")) {
|
|
1693
|
+
category = ErrorCategory.VALIDATION;
|
|
1694
|
+
severity = ErrorSeverity.HIGH;
|
|
1695
|
+
} else if (error.message.includes("security") || error.message.includes("injection")) {
|
|
1696
|
+
category = ErrorCategory.SECURITY;
|
|
1697
|
+
severity = ErrorSeverity.CRITICAL;
|
|
1698
|
+
}
|
|
1699
|
+
return new EnhancedStateMachineError(
|
|
1700
|
+
error.message,
|
|
1701
|
+
context.errorContext || {},
|
|
1702
|
+
{
|
|
1703
|
+
category,
|
|
1704
|
+
severity,
|
|
1705
|
+
cause: error,
|
|
1706
|
+
additionalData: context
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
getAnalytics() {
|
|
1711
|
+
return this.analytics;
|
|
1712
|
+
}
|
|
1713
|
+
enable() {
|
|
1714
|
+
this.enabled = true;
|
|
1715
|
+
}
|
|
1716
|
+
disable() {
|
|
1717
|
+
this.enabled = false;
|
|
1718
|
+
}
|
|
1719
|
+
isEnabled() {
|
|
1720
|
+
return this.enabled;
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
function createDefaultErrorHandler() {
|
|
1724
|
+
return new ErrorHandler();
|
|
1725
|
+
}
|
|
1726
|
+
function createEnhancedError(message, context, options) {
|
|
1727
|
+
return new EnhancedStateMachineError(message, context, options);
|
|
1728
|
+
}
|
|
1729
|
+
function isRecoverableError(error) {
|
|
1730
|
+
if (error instanceof EnhancedStateMachineError) {
|
|
1731
|
+
return error.recoverable;
|
|
1732
|
+
}
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/state_machine.ts
|
|
1737
|
+
var ConsoleLogger = {
|
|
1738
|
+
debug: () => {
|
|
1739
|
+
},
|
|
1740
|
+
// По умолчанию выключено для Lite
|
|
1741
|
+
info: () => {
|
|
1742
|
+
},
|
|
1743
|
+
warn: (msg, ctx) => console.warn(msg, ctx),
|
|
1744
|
+
error: (msg, ctx, err) => console.error(msg, ctx, err)
|
|
1745
|
+
};
|
|
1746
|
+
var StateMachine = class _StateMachine {
|
|
1747
|
+
// Приватные поля для зависимостей
|
|
1748
|
+
logger;
|
|
1749
|
+
monitor;
|
|
1750
|
+
scheduler;
|
|
1751
|
+
errorHandler;
|
|
1752
|
+
// Свойства
|
|
1753
|
+
states;
|
|
1754
|
+
events;
|
|
1755
|
+
stateAttribute;
|
|
1756
|
+
onError;
|
|
1757
|
+
adaptee;
|
|
1758
|
+
context;
|
|
1759
|
+
historyMap = /* @__PURE__ */ new Map();
|
|
1760
|
+
initialState;
|
|
1761
|
+
persistenceAdapter;
|
|
1762
|
+
activeTimers = /* @__PURE__ */ new Map();
|
|
1763
|
+
stateEntryTimes = /* @__PURE__ */ new Map();
|
|
1764
|
+
// Event Queue Infrastructure (SCXML Run-to-Completion)
|
|
1765
|
+
externalQueue = [];
|
|
1766
|
+
internalQueue = [];
|
|
1767
|
+
isProcessing = false;
|
|
1768
|
+
eventIdCounter = 0;
|
|
1769
|
+
transitionDepth = 0;
|
|
1770
|
+
MAX_TRANSITION_DEPTH = 100;
|
|
1771
|
+
// Optional configuration
|
|
1772
|
+
transitionTimeout;
|
|
1773
|
+
errorState;
|
|
1774
|
+
abortOnExitError;
|
|
1775
|
+
maxQueueDepth = 1e3;
|
|
1776
|
+
// Transition state visibility
|
|
1777
|
+
_isTransitioning = false;
|
|
1778
|
+
_targetState;
|
|
1779
|
+
get isTransitioning() {
|
|
1780
|
+
return this._isTransitioning;
|
|
1781
|
+
}
|
|
1782
|
+
get targetState() {
|
|
1783
|
+
return this._targetState;
|
|
1784
|
+
}
|
|
1785
|
+
// Геттеры и сеттеры
|
|
1786
|
+
set currentState(state) {
|
|
1787
|
+
if (!this.adaptee) throw new StateMachineError("no adaptee", { state });
|
|
1788
|
+
this.setCurrentState(state, this.adaptee);
|
|
1789
|
+
}
|
|
1790
|
+
get currentState() {
|
|
1791
|
+
if (!this.adaptee) {
|
|
1792
|
+
const s = this.getCurrentState();
|
|
1793
|
+
throw new StateMachineError("no adaptee", s !== void 0 ? { state: s } : {});
|
|
1794
|
+
}
|
|
1795
|
+
return this.getCurrentState(this.adaptee) ?? "";
|
|
1796
|
+
}
|
|
1797
|
+
// Конструктор
|
|
1798
|
+
constructor(config, adaptee, options) {
|
|
1799
|
+
this.initialState = config.initialState;
|
|
1800
|
+
this.logger = options?.logger ?? ConsoleLogger;
|
|
1801
|
+
this.monitor = options?.monitor ?? createDefaultMonitor();
|
|
1802
|
+
this.scheduler = options?.scheduler ?? createDefaultScheduler();
|
|
1803
|
+
this.errorHandler = options?.errorHandler ?? createDefaultErrorHandler();
|
|
1804
|
+
if (adaptee) {
|
|
1805
|
+
if (!isAdapter(adaptee)) {
|
|
1806
|
+
this.adaptee = new MemoryAdapter(adaptee);
|
|
1807
|
+
} else {
|
|
1808
|
+
this.adaptee = adaptee;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (options?.transitionTimeout !== void 0) {
|
|
1812
|
+
this.transitionTimeout = options.transitionTimeout;
|
|
1813
|
+
}
|
|
1814
|
+
if (options?.errorState !== void 0) {
|
|
1815
|
+
this.errorState = options.errorState;
|
|
1816
|
+
}
|
|
1817
|
+
if (options?.abortOnExitError !== void 0) {
|
|
1818
|
+
this.abortOnExitError = options.abortOnExitError;
|
|
1819
|
+
}
|
|
1820
|
+
if (options?.maxQueueDepth !== void 0) {
|
|
1821
|
+
this.maxQueueDepth = options.maxQueueDepth;
|
|
1822
|
+
}
|
|
1823
|
+
if (config.onError !== void 0) {
|
|
1824
|
+
this.onError = config.onError;
|
|
1825
|
+
}
|
|
1826
|
+
this.stateAttribute = config.stateAttribute;
|
|
1827
|
+
this.states = /* @__PURE__ */ new Map();
|
|
1828
|
+
this.events = new Map(
|
|
1829
|
+
Object.entries(config.events).map(([name, value]) => [
|
|
1830
|
+
name,
|
|
1831
|
+
{
|
|
1832
|
+
name,
|
|
1833
|
+
...value,
|
|
1834
|
+
transitions: value.transitions.map((t) => ({
|
|
1835
|
+
...t,
|
|
1836
|
+
// Support 'action' alias for 'onTransition'
|
|
1837
|
+
onTransition: t.onTransition || t.action
|
|
1838
|
+
}))
|
|
1839
|
+
}
|
|
1840
|
+
])
|
|
1841
|
+
);
|
|
1842
|
+
this.processStates(config.states);
|
|
1843
|
+
if (adaptee) {
|
|
1844
|
+
this.persistenceAdapter = adaptee;
|
|
1845
|
+
this.setInitialState(config.initialState);
|
|
1846
|
+
}
|
|
1847
|
+
this.logger.info("StateMachine initialized", { name: config.name });
|
|
1848
|
+
}
|
|
1849
|
+
// Основные публичные методы
|
|
1850
|
+
setContext(context) {
|
|
1851
|
+
this.context = context;
|
|
1852
|
+
}
|
|
1853
|
+
enqueueEvent(eventName, obj, args, type) {
|
|
1854
|
+
if (this.externalQueue.length + this.internalQueue.length >= this.maxQueueDepth) {
|
|
1855
|
+
const _s0 = this.getCurrentState(obj);
|
|
1856
|
+
return Promise.reject(
|
|
1857
|
+
new StateMachineError("Event queue overflow \u2014 possible infinite loop", {
|
|
1858
|
+
/* c8 ignore next */
|
|
1859
|
+
..._s0 !== void 0 ? { state: _s0 } : {},
|
|
1860
|
+
event: eventName
|
|
1861
|
+
})
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
if (type === "external") {
|
|
1865
|
+
return new Promise((resolve, reject) => {
|
|
1866
|
+
this.externalQueue.push({
|
|
1867
|
+
id: `ext_${++this.eventIdCounter}`,
|
|
1868
|
+
eventName,
|
|
1869
|
+
obj,
|
|
1870
|
+
args,
|
|
1871
|
+
resolve,
|
|
1872
|
+
reject,
|
|
1873
|
+
timestamp: Date.now(),
|
|
1874
|
+
type: "external"
|
|
1875
|
+
});
|
|
1876
|
+
this.scheduleProcessing();
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
this.internalQueue.push({
|
|
1880
|
+
id: `int_${++this.eventIdCounter}`,
|
|
1881
|
+
eventName,
|
|
1882
|
+
obj,
|
|
1883
|
+
args,
|
|
1884
|
+
timestamp: Date.now(),
|
|
1885
|
+
type: "internal"
|
|
1886
|
+
});
|
|
1887
|
+
return Promise.resolve(true);
|
|
1888
|
+
}
|
|
1889
|
+
raiseEvent(eventName, obj, ...args) {
|
|
1890
|
+
this.internalQueue.push({
|
|
1891
|
+
id: `int_${++this.eventIdCounter}`,
|
|
1892
|
+
eventName,
|
|
1893
|
+
obj,
|
|
1894
|
+
args,
|
|
1895
|
+
timestamp: Date.now(),
|
|
1896
|
+
type: "internal"
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
scheduleProcessing() {
|
|
1900
|
+
if (this.isProcessing) return;
|
|
1901
|
+
queueMicrotask(() => this.processQueues());
|
|
1902
|
+
}
|
|
1903
|
+
async processQueues() {
|
|
1904
|
+
if (this.isProcessing) return;
|
|
1905
|
+
this.isProcessing = true;
|
|
1906
|
+
try {
|
|
1907
|
+
while (this.internalQueue.length > 0 || this.externalQueue.length > 0) {
|
|
1908
|
+
if (this.transitionDepth >= this.MAX_TRANSITION_DEPTH) {
|
|
1909
|
+
const _s1 = this.getCurrentState();
|
|
1910
|
+
const error = new StateMachineError(
|
|
1911
|
+
"Max transition depth exceeded \u2014 possible infinite loop",
|
|
1912
|
+
/* c8 ignore next */
|
|
1913
|
+
{ ..._s1 !== void 0 ? { state: _s1 } : {}, event: "processQueues" }
|
|
1914
|
+
);
|
|
1915
|
+
while (this.externalQueue.length > 0) {
|
|
1916
|
+
const evt = this.externalQueue.shift();
|
|
1917
|
+
evt.reject?.(error);
|
|
1918
|
+
}
|
|
1919
|
+
this.internalQueue.length = 0;
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1922
|
+
if (this.internalQueue.length > 0) {
|
|
1923
|
+
const evt = this.internalQueue.shift();
|
|
1924
|
+
this.transitionDepth++;
|
|
1925
|
+
try {
|
|
1926
|
+
await this.executeQueuedTransition(evt);
|
|
1927
|
+
} finally {
|
|
1928
|
+
this.transitionDepth--;
|
|
1929
|
+
}
|
|
1930
|
+
} else {
|
|
1931
|
+
const evt = this.externalQueue.shift();
|
|
1932
|
+
this.transitionDepth++;
|
|
1933
|
+
try {
|
|
1934
|
+
const result = await this.executeQueuedTransition(evt);
|
|
1935
|
+
evt.resolve?.(result);
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
evt.reject?.(error);
|
|
1938
|
+
} finally {
|
|
1939
|
+
this.transitionDepth--;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
} finally {
|
|
1944
|
+
this.isProcessing = false;
|
|
1945
|
+
this.transitionDepth = 0;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
async executeQueuedTransition(queuedEvent) {
|
|
1949
|
+
const { eventName, obj, args } = queuedEvent;
|
|
1950
|
+
const targetObj = obj;
|
|
1951
|
+
let currentStateRaw = this.getCurrentState(targetObj);
|
|
1952
|
+
if (!currentStateRaw) {
|
|
1953
|
+
this.setInitialState(this.initialState, targetObj);
|
|
1954
|
+
currentStateRaw = this.getCurrentState(targetObj);
|
|
1955
|
+
}
|
|
1956
|
+
const currentState = currentStateRaw ?? "";
|
|
1957
|
+
let event = this.events.get(eventName);
|
|
1958
|
+
let transitions = event ? event.transitions.filter(
|
|
1959
|
+
(t) => this.isTransitionPossible(t, currentState)
|
|
1960
|
+
) : [];
|
|
1961
|
+
if (!transitions.length) {
|
|
1962
|
+
const wildcardEvent = this.events.get("*");
|
|
1963
|
+
if (wildcardEvent) {
|
|
1964
|
+
const wildcardTransitions = wildcardEvent.transitions.filter(
|
|
1965
|
+
(t) => this.isTransitionPossible(t, currentState)
|
|
1966
|
+
);
|
|
1967
|
+
if (wildcardTransitions.length > 0) {
|
|
1968
|
+
event = wildcardEvent;
|
|
1969
|
+
transitions = wildcardTransitions;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (!event || !transitions.length) {
|
|
1974
|
+
throw new StateMachineError(
|
|
1975
|
+
`Invalid event: ${eventName} for state: ${currentState}`,
|
|
1976
|
+
{ state: currentState, event: eventName }
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const allowedTransition = await this.getAllowedTransitions(
|
|
1980
|
+
targetObj,
|
|
1981
|
+
transitions,
|
|
1982
|
+
...args
|
|
1983
|
+
).catch((error) => {
|
|
1984
|
+
this.logger.error(
|
|
1985
|
+
"Error determining allowed transition",
|
|
1986
|
+
{ event: eventName, state: currentState },
|
|
1987
|
+
/* c8 ignore next */
|
|
1988
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1989
|
+
);
|
|
1990
|
+
return void 0;
|
|
1991
|
+
});
|
|
1992
|
+
if (!allowedTransition) {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
const toState = await this.applyTransition(
|
|
1996
|
+
targetObj,
|
|
1997
|
+
currentState,
|
|
1998
|
+
allowedTransition,
|
|
1999
|
+
args,
|
|
2000
|
+
eventName,
|
|
2001
|
+
event
|
|
2002
|
+
).catch((error) => {
|
|
2003
|
+
this.logger.error(
|
|
2004
|
+
"Error applying transition",
|
|
2005
|
+
{
|
|
2006
|
+
event: eventName,
|
|
2007
|
+
state: currentState,
|
|
2008
|
+
transition: `${allowedTransition.from} -> ${allowedTransition.to}`
|
|
2009
|
+
},
|
|
2010
|
+
/* c8 ignore next */
|
|
2011
|
+
error instanceof Error ? error : new Error(String(error))
|
|
2012
|
+
);
|
|
2013
|
+
if (this.errorHandler.isEnabled()) {
|
|
2014
|
+
this.monitor.recordError(
|
|
2015
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2016
|
+
{ state: currentState, event: eventName }
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
throw error;
|
|
2020
|
+
});
|
|
2021
|
+
if (!toState) {
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
this.setCurrentState(toState.name, targetObj);
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
2027
|
+
async fireEvent(eventName, obj, ...args) {
|
|
2028
|
+
let targetObj;
|
|
2029
|
+
if (!obj) {
|
|
2030
|
+
if (this.adaptee) targetObj = this.adaptee;
|
|
2031
|
+
else
|
|
2032
|
+
throw new StateMachineError("no adaptee or object passed", {
|
|
2033
|
+
event: String(eventName)
|
|
2034
|
+
});
|
|
2035
|
+
} else if (!isAdapter(obj)) {
|
|
2036
|
+
args.unshift(obj);
|
|
2037
|
+
if (this.adaptee) targetObj = this.adaptee;
|
|
2038
|
+
else
|
|
2039
|
+
throw new StateMachineError("no adaptee or object passed", {
|
|
2040
|
+
event: String(eventName)
|
|
2041
|
+
});
|
|
2042
|
+
} else {
|
|
2043
|
+
targetObj = obj;
|
|
2044
|
+
}
|
|
2045
|
+
return this.enqueueEvent(String(eventName), targetObj, args, "external");
|
|
2046
|
+
}
|
|
2047
|
+
getQueueDepth() {
|
|
2048
|
+
return {
|
|
2049
|
+
internal: this.internalQueue.length,
|
|
2050
|
+
external: this.externalQueue.length,
|
|
2051
|
+
total: this.internalQueue.length + this.externalQueue.length
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
getQueuedEvents() {
|
|
2055
|
+
const now = Date.now();
|
|
2056
|
+
const mapEvent = (evt) => ({
|
|
2057
|
+
id: evt.id,
|
|
2058
|
+
event: evt.eventName,
|
|
2059
|
+
age: now - evt.timestamp,
|
|
2060
|
+
type: evt.type
|
|
2061
|
+
});
|
|
2062
|
+
return [
|
|
2063
|
+
...this.internalQueue.map(mapEvent),
|
|
2064
|
+
...this.externalQueue.map(mapEvent)
|
|
2065
|
+
];
|
|
2066
|
+
}
|
|
2067
|
+
isProcessingEvents() {
|
|
2068
|
+
return this.isProcessing;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Checks if an event can be fired in the current state.
|
|
2072
|
+
*
|
|
2073
|
+
* @param eventName - The name of the event to check.
|
|
2074
|
+
* @param adaptee - Optional adapter/object to check against (defaults to internal adaptee).
|
|
2075
|
+
* @returns boolean - True if the event has a valid transition from the current state (guards are not executed).
|
|
2076
|
+
*/
|
|
2077
|
+
canFireEvent(eventName, adaptee) {
|
|
2078
|
+
const targetAdaptee = adaptee || this.adaptee;
|
|
2079
|
+
if (!targetAdaptee) return false;
|
|
2080
|
+
const currentState = this.getCurrentState(targetAdaptee);
|
|
2081
|
+
const effectiveState = currentState === "" ? this.getInitialCompositeState(this.initialState) : currentState;
|
|
2082
|
+
if (effectiveState === void 0) return false;
|
|
2083
|
+
const event = this.events.get(String(eventName));
|
|
2084
|
+
if (!event || !event.transitions.some(
|
|
2085
|
+
(t) => this.isTransitionPossible(t, effectiveState)
|
|
2086
|
+
)) {
|
|
2087
|
+
const wildcardEvent = this.events.get("*");
|
|
2088
|
+
if (wildcardEvent) {
|
|
2089
|
+
return wildcardEvent.transitions.some(
|
|
2090
|
+
(t) => this.isTransitionPossible(t, effectiveState)
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
getAvailableEvents(adaptee) {
|
|
2098
|
+
const available = [];
|
|
2099
|
+
for (const [name] of this.events) {
|
|
2100
|
+
if (this.canFireEvent(name, adaptee)) {
|
|
2101
|
+
available.push(String(name));
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return available;
|
|
2105
|
+
}
|
|
2106
|
+
async reset(adaptee) {
|
|
2107
|
+
const targetAdaptee = adaptee || this.adaptee;
|
|
2108
|
+
if (!targetAdaptee) {
|
|
2109
|
+
const _s2 = this.getCurrentState();
|
|
2110
|
+
throw new StateMachineError("no adaptee", _s2 !== void 0 ? { state: _s2 } : {});
|
|
2111
|
+
}
|
|
2112
|
+
this.historyMap.clear();
|
|
2113
|
+
for (const timers of this.activeTimers.values()) {
|
|
2114
|
+
for (const id of timers) {
|
|
2115
|
+
this.clearTimer(id);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
this.activeTimers.clear();
|
|
2119
|
+
this.stateEntryTimes.clear();
|
|
2120
|
+
this.setInitialState(this.initialState, targetAdaptee);
|
|
2121
|
+
}
|
|
2122
|
+
getStateHistory() {
|
|
2123
|
+
return Object.fromEntries(this.historyMap);
|
|
2124
|
+
}
|
|
2125
|
+
getCurrentStateInfo() {
|
|
2126
|
+
const currentState = this.getCurrentState();
|
|
2127
|
+
if (currentState === void 0) return void 0;
|
|
2128
|
+
const isComposite = currentState.includes("|");
|
|
2129
|
+
if (isComposite) {
|
|
2130
|
+
const activeStates = currentState.split("|").filter(Boolean);
|
|
2131
|
+
const regions2 = activeStates.map((s) => this.getRegionKey(s));
|
|
2132
|
+
return {
|
|
2133
|
+
name: currentState,
|
|
2134
|
+
isComposite: true,
|
|
2135
|
+
regions: regions2,
|
|
2136
|
+
children: activeStates
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const state = this.states.get(currentState);
|
|
2140
|
+
if (!state) return void 0;
|
|
2141
|
+
const parent = currentState.includes(".") ? currentState.split(".").slice(0, -1).join(".") : void 0;
|
|
2142
|
+
const children = this.getDirectChildren(currentState);
|
|
2143
|
+
const regions = state.regions ? Object.keys(state.regions) : void 0;
|
|
2144
|
+
return {
|
|
2145
|
+
name: currentState,
|
|
2146
|
+
...state.display !== void 0 ? { display: state.display } : {},
|
|
2147
|
+
isComposite: Boolean(state.regions),
|
|
2148
|
+
...regions !== void 0 ? { regions } : {},
|
|
2149
|
+
...parent !== void 0 ? { parent } : {},
|
|
2150
|
+
...children.length ? { children } : {}
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Checks if the machine is in a specific state.
|
|
2155
|
+
* Supports hierarchical states (e.g. 'parent' matches 'parent.child').
|
|
2156
|
+
*
|
|
2157
|
+
* @param expectedState - The state name to check.
|
|
2158
|
+
* @param adaptee - Optional adapter to check against.
|
|
2159
|
+
* @returns boolean - True if the current state matches or is a substate of the expected state.
|
|
2160
|
+
*/
|
|
2161
|
+
isInState(expectedState, adaptee) {
|
|
2162
|
+
const currentState = this.getCurrentState(adaptee);
|
|
2163
|
+
if (!currentState) return expectedState === "";
|
|
2164
|
+
const currentParts = currentState.split("|").sort();
|
|
2165
|
+
const expectedParts = expectedState.split("|").sort();
|
|
2166
|
+
if (currentParts.length !== expectedParts.length) return false;
|
|
2167
|
+
return currentParts.every((part, index) => part === expectedParts[index]);
|
|
2168
|
+
}
|
|
2169
|
+
attachToObject(object, eventMap) {
|
|
2170
|
+
for (const objectEventName in eventMap) {
|
|
2171
|
+
if (eventMap.hasOwnProperty(objectEventName)) {
|
|
2172
|
+
const stateMachineEventName = eventMap[objectEventName];
|
|
2173
|
+
if (stateMachineEventName === void 0) continue;
|
|
2174
|
+
if (typeof object.addEventListener === "function") {
|
|
2175
|
+
object.addEventListener(objectEventName, (...args) => {
|
|
2176
|
+
this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2177
|
+
(e) => this.logger.error(
|
|
2178
|
+
"Error firing event",
|
|
2179
|
+
{
|
|
2180
|
+
objectEventName,
|
|
2181
|
+
stateMachineEventName
|
|
2182
|
+
},
|
|
2183
|
+
/* c8 ignore next */
|
|
2184
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2185
|
+
)
|
|
2186
|
+
);
|
|
2187
|
+
});
|
|
2188
|
+
} else if (typeof object.on === "function") {
|
|
2189
|
+
object.on(objectEventName, (...args) => {
|
|
2190
|
+
this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2191
|
+
(e) => this.logger.error(
|
|
2192
|
+
"Error firing event",
|
|
2193
|
+
{
|
|
2194
|
+
objectEventName,
|
|
2195
|
+
stateMachineEventName
|
|
2196
|
+
},
|
|
2197
|
+
/* c8 ignore next */
|
|
2198
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2199
|
+
)
|
|
2200
|
+
);
|
|
2201
|
+
});
|
|
2202
|
+
} else {
|
|
2203
|
+
object[`on${objectEventName}`] = async (...args) => {
|
|
2204
|
+
return this.fireEvent(stateMachineEventName, object, ...args).catch(
|
|
2205
|
+
(e) => this.logger.error(
|
|
2206
|
+
"Error firing event",
|
|
2207
|
+
{
|
|
2208
|
+
objectEventName,
|
|
2209
|
+
stateMachineEventName
|
|
2210
|
+
},
|
|
2211
|
+
/* c8 ignore next */
|
|
2212
|
+
e instanceof Error ? e : new Error(String(e))
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// Методы для работы с состоянием
|
|
2221
|
+
async saveState(adapter) {
|
|
2222
|
+
const targetAdapter = adapter || this.persistenceAdapter;
|
|
2223
|
+
if (!targetAdapter) {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const currentState = this.getCurrentState() ?? "";
|
|
2227
|
+
const history = Object.fromEntries(this.historyMap);
|
|
2228
|
+
const stateData = {
|
|
2229
|
+
currentState,
|
|
2230
|
+
history,
|
|
2231
|
+
stateEntryTimes: Object.fromEntries(this.stateEntryTimes)
|
|
2232
|
+
};
|
|
2233
|
+
await targetAdapter.save(stateData);
|
|
2234
|
+
}
|
|
2235
|
+
async restoreState(adapter) {
|
|
2236
|
+
const targetAdapter = adapter || this.persistenceAdapter;
|
|
2237
|
+
if (!targetAdapter) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const result = await targetAdapter.restore();
|
|
2241
|
+
this.validateCompositeState(result.currentState);
|
|
2242
|
+
this.historyMap = new Map(Object.entries(result.history));
|
|
2243
|
+
if (result.stateEntryTimes) {
|
|
2244
|
+
this.stateEntryTimes = new Map(Object.entries(result.stateEntryTimes));
|
|
2245
|
+
}
|
|
2246
|
+
this.setCurrentState(result.currentState);
|
|
2247
|
+
this.resumeTimers();
|
|
2248
|
+
}
|
|
2249
|
+
// Статические методы
|
|
2250
|
+
static fromData(config, initialState, context, options) {
|
|
2251
|
+
const sm = new _StateMachine(config, context, options);
|
|
2252
|
+
if (sm.adaptee) {
|
|
2253
|
+
sm.setCurrentState(initialState || config.initialState);
|
|
2254
|
+
}
|
|
2255
|
+
return sm;
|
|
2256
|
+
}
|
|
2257
|
+
static fromJSON(jsonData, obj, options) {
|
|
2258
|
+
const parsedData = JSON.parse(jsonData);
|
|
2259
|
+
const { config, currentState, historyMap, stateEntryTimes } = parsedData;
|
|
2260
|
+
const deserializedStates = _StateMachine.deserializeStates(
|
|
2261
|
+
config.states
|
|
2262
|
+
);
|
|
2263
|
+
const deserializedEvents = _StateMachine.deserializeEvents(
|
|
2264
|
+
config.events
|
|
2265
|
+
);
|
|
2266
|
+
const smConfig = {
|
|
2267
|
+
name: "DeserializedStateMachine",
|
|
2268
|
+
initialState: config.initialState,
|
|
2269
|
+
stateAttribute: config.stateAttribute,
|
|
2270
|
+
states: deserializedStates,
|
|
2271
|
+
events: deserializedEvents,
|
|
2272
|
+
onError: _StateMachine.deserializeAction(config.onError)
|
|
2273
|
+
};
|
|
2274
|
+
const sm = new _StateMachine(
|
|
2275
|
+
smConfig,
|
|
2276
|
+
obj,
|
|
2277
|
+
options
|
|
2278
|
+
);
|
|
2279
|
+
sm.historyMap = new Map(historyMap);
|
|
2280
|
+
if (stateEntryTimes) {
|
|
2281
|
+
sm.stateEntryTimes = new Map(stateEntryTimes);
|
|
2282
|
+
}
|
|
2283
|
+
if (sm.adaptee && currentState) {
|
|
2284
|
+
sm.setCurrentState(currentState);
|
|
2285
|
+
sm.resumeTimers();
|
|
2286
|
+
}
|
|
2287
|
+
return sm;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Securely deserializes a StateMachine from JSON string (Async).
|
|
2291
|
+
* Verifies cryptographic hashes of serialized functions.
|
|
2292
|
+
*/
|
|
2293
|
+
static async fromSecureJSON(jsonData, obj, options) {
|
|
2294
|
+
const parsedData = JSON.parse(jsonData);
|
|
2295
|
+
const { config, currentState, historyMap, stateEntryTimes } = parsedData;
|
|
2296
|
+
const deserializedStates = await _StateMachine.deserializeStatesAsync(config.states);
|
|
2297
|
+
const deserializedEvents = await _StateMachine.deserializeEventsAsync(config.events);
|
|
2298
|
+
const smConfig = {
|
|
2299
|
+
name: "DeserializedStateMachine",
|
|
2300
|
+
initialState: config.initialState,
|
|
2301
|
+
stateAttribute: config.stateAttribute,
|
|
2302
|
+
states: deserializedStates,
|
|
2303
|
+
events: deserializedEvents,
|
|
2304
|
+
onError: await _StateMachine.deserializeActionAsync(
|
|
2305
|
+
config.onError
|
|
2306
|
+
)
|
|
2307
|
+
};
|
|
2308
|
+
const sm = new _StateMachine(
|
|
2309
|
+
smConfig,
|
|
2310
|
+
obj,
|
|
2311
|
+
options
|
|
2312
|
+
);
|
|
2313
|
+
sm.historyMap = new Map(historyMap);
|
|
2314
|
+
if (stateEntryTimes) {
|
|
2315
|
+
sm.stateEntryTimes = new Map(stateEntryTimes);
|
|
2316
|
+
}
|
|
2317
|
+
if (sm.adaptee && currentState) {
|
|
2318
|
+
sm.setCurrentState(currentState);
|
|
2319
|
+
sm.resumeTimers();
|
|
2320
|
+
}
|
|
2321
|
+
return sm;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Deserialize states configuration (Async)
|
|
2325
|
+
*/
|
|
2326
|
+
static async deserializeStatesAsync(statesConfig) {
|
|
2327
|
+
const result = {};
|
|
2328
|
+
for (const [name, stateData] of Object.entries(statesConfig)) {
|
|
2329
|
+
const deserializedState = {
|
|
2330
|
+
...stateData,
|
|
2331
|
+
onBeforeEnter: await _StateMachine.deserializeActionAsync(
|
|
2332
|
+
stateData.onBeforeEnter
|
|
2333
|
+
),
|
|
2334
|
+
onEnter: await _StateMachine.deserializeActionAsync(stateData.onEnter),
|
|
2335
|
+
onAfterEnter: await _StateMachine.deserializeActionAsync(
|
|
2336
|
+
stateData.onAfterEnter
|
|
2337
|
+
),
|
|
2338
|
+
onBeforeExit: await _StateMachine.deserializeActionAsync(
|
|
2339
|
+
stateData.onBeforeExit
|
|
2340
|
+
),
|
|
2341
|
+
onExit: await _StateMachine.deserializeActionAsync(stateData.onExit),
|
|
2342
|
+
onAfterExit: await _StateMachine.deserializeActionAsync(
|
|
2343
|
+
stateData.onAfterExit
|
|
2344
|
+
),
|
|
2345
|
+
onError: await _StateMachine.deserializeActionAsync(stateData.onError)
|
|
2346
|
+
};
|
|
2347
|
+
if (stateData.invoke && Array.isArray(stateData.invoke)) {
|
|
2348
|
+
deserializedState.invoke = await Promise.all(
|
|
2349
|
+
stateData.invoke.map(async (inv) => ({
|
|
2350
|
+
...inv,
|
|
2351
|
+
cond: await _StateMachine.deserializeActionAsync(inv.cond),
|
|
2352
|
+
action: await _StateMachine.deserializeActionAsync(inv.action)
|
|
2353
|
+
}))
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
result[name] = deserializedState;
|
|
2357
|
+
}
|
|
2358
|
+
return result;
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Deserialize states configuration
|
|
2362
|
+
*/
|
|
2363
|
+
static deserializeStates(statesConfig) {
|
|
2364
|
+
return Object.entries(statesConfig).reduce(
|
|
2365
|
+
(acc, [name, stateData]) => {
|
|
2366
|
+
const deserializedState = {
|
|
2367
|
+
...stateData,
|
|
2368
|
+
onBeforeEnter: _StateMachine.deserializeAction(
|
|
2369
|
+
stateData.onBeforeEnter
|
|
2370
|
+
),
|
|
2371
|
+
onEnter: _StateMachine.deserializeAction(stateData.onEnter),
|
|
2372
|
+
onAfterEnter: _StateMachine.deserializeAction(stateData.onAfterEnter),
|
|
2373
|
+
onBeforeExit: _StateMachine.deserializeAction(stateData.onBeforeExit),
|
|
2374
|
+
onExit: _StateMachine.deserializeAction(stateData.onExit),
|
|
2375
|
+
onAfterExit: _StateMachine.deserializeAction(stateData.onAfterExit),
|
|
2376
|
+
onError: _StateMachine.deserializeAction(stateData.onError)
|
|
2377
|
+
};
|
|
2378
|
+
if (stateData.invoke && Array.isArray(stateData.invoke)) {
|
|
2379
|
+
deserializedState.invoke = stateData.invoke.map((inv) => ({
|
|
2380
|
+
...inv,
|
|
2381
|
+
cond: _StateMachine.deserializeAction(inv.cond),
|
|
2382
|
+
action: _StateMachine.deserializeAction(inv.action)
|
|
2383
|
+
}));
|
|
2384
|
+
}
|
|
2385
|
+
acc[name] = deserializedState;
|
|
2386
|
+
return acc;
|
|
2387
|
+
},
|
|
2388
|
+
{}
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Deserialize events configuration (Async)
|
|
2393
|
+
*/
|
|
2394
|
+
static async deserializeEventsAsync(eventsConfig) {
|
|
2395
|
+
const result = {};
|
|
2396
|
+
for (const [name, eventData] of Object.entries(eventsConfig)) {
|
|
2397
|
+
result[name] = {
|
|
2398
|
+
...eventData,
|
|
2399
|
+
onBefore: await _StateMachine.deserializeActionAsync(eventData.onBefore),
|
|
2400
|
+
onAfter: await _StateMachine.deserializeActionAsync(eventData.onAfter),
|
|
2401
|
+
onSuccess: await _StateMachine.deserializeActionAsync(
|
|
2402
|
+
eventData.onSuccess
|
|
2403
|
+
),
|
|
2404
|
+
onError: await _StateMachine.deserializeActionAsync(eventData.onError),
|
|
2405
|
+
transitions: await Promise.all(
|
|
2406
|
+
eventData.transitions.map(
|
|
2407
|
+
(transitionData) => _StateMachine.deserializeTransitionAsync(transitionData)
|
|
2408
|
+
)
|
|
2409
|
+
)
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
return result;
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Deserialize events configuration
|
|
2416
|
+
*/
|
|
2417
|
+
static deserializeEvents(eventsConfig) {
|
|
2418
|
+
return Object.entries(eventsConfig).reduce(
|
|
2419
|
+
(acc, [name, eventData]) => {
|
|
2420
|
+
acc[name] = {
|
|
2421
|
+
...eventData,
|
|
2422
|
+
onBefore: _StateMachine.deserializeAction(eventData.onBefore),
|
|
2423
|
+
onAfter: _StateMachine.deserializeAction(eventData.onAfter),
|
|
2424
|
+
onSuccess: _StateMachine.deserializeAction(eventData.onSuccess),
|
|
2425
|
+
onError: _StateMachine.deserializeAction(eventData.onError),
|
|
2426
|
+
transitions: eventData.transitions.map(
|
|
2427
|
+
(transitionData) => _StateMachine.deserializeTransition(transitionData)
|
|
2428
|
+
)
|
|
2429
|
+
};
|
|
2430
|
+
return acc;
|
|
2431
|
+
},
|
|
2432
|
+
{}
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Deserialize transition configuration (Async)
|
|
2437
|
+
*/
|
|
2438
|
+
static async deserializeTransitionAsync(transitionData) {
|
|
2439
|
+
return {
|
|
2440
|
+
...transitionData,
|
|
2441
|
+
guard: await _StateMachine.deserializeActionAsync(transitionData.guard),
|
|
2442
|
+
// Support 'action' alias for 'onTransition'
|
|
2443
|
+
onTransition: await _StateMachine.deserializeActionAsync(
|
|
2444
|
+
transitionData.onTransition || transitionData.action
|
|
2445
|
+
),
|
|
2446
|
+
onError: await _StateMachine.deserializeActionAsync(
|
|
2447
|
+
transitionData.onError
|
|
2448
|
+
)
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Deserialize transition configuration
|
|
2453
|
+
*/
|
|
2454
|
+
static deserializeTransition(transitionData) {
|
|
2455
|
+
return {
|
|
2456
|
+
...transitionData,
|
|
2457
|
+
guard: _StateMachine.deserializeAction(transitionData.guard),
|
|
2458
|
+
// Support 'action' alias for 'onTransition' for compatibility
|
|
2459
|
+
onTransition: _StateMachine.deserializeAction(
|
|
2460
|
+
transitionData.onTransition || transitionData.action
|
|
2461
|
+
),
|
|
2462
|
+
onError: _StateMachine.deserializeAction(transitionData.onError)
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Static method for deserializing actions (Async)
|
|
2467
|
+
*/
|
|
2468
|
+
static async deserializeActionAsync(action) {
|
|
2469
|
+
if (!action) return action;
|
|
2470
|
+
if (typeof action === "object" && action.type === "function") {
|
|
2471
|
+
return safeFunctionSerializer.deserializeActionAsync(action);
|
|
2472
|
+
}
|
|
2473
|
+
return _StateMachine.deserializeAction(action);
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Static method for deserializing actions (now using safe deserializer)
|
|
2477
|
+
*/
|
|
2478
|
+
static deserializeAction(action) {
|
|
2479
|
+
if (!action) return action;
|
|
2480
|
+
if (typeof action === "object" && action.type === "function") {
|
|
2481
|
+
return safeFunctionSerializer.deserializeAction(action);
|
|
2482
|
+
}
|
|
2483
|
+
if (typeof action === "string") {
|
|
2484
|
+
const trimmed = action.trim();
|
|
2485
|
+
if (trimmed.startsWith("function") || trimmed.startsWith("(") || trimmed.includes("=>")) {
|
|
2486
|
+
const safeFn = safeFunctionSerializer.deserializeLegacyString(action);
|
|
2487
|
+
if (safeFn) {
|
|
2488
|
+
return safeFn;
|
|
2489
|
+
}
|
|
2490
|
+
return void 0;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return action;
|
|
2494
|
+
}
|
|
2495
|
+
static fromJSONWithContext(jsonData, context, options) {
|
|
2496
|
+
const sm = _StateMachine.fromJSON(
|
|
2497
|
+
jsonData,
|
|
2498
|
+
void 0,
|
|
2499
|
+
options
|
|
2500
|
+
);
|
|
2501
|
+
if (context !== void 0) {
|
|
2502
|
+
sm.setContext(context);
|
|
2503
|
+
}
|
|
2504
|
+
return sm;
|
|
2505
|
+
}
|
|
2506
|
+
// Приватные методы
|
|
2507
|
+
setCurrentState(state, obj) {
|
|
2508
|
+
const adaptee = obj || this.adaptee;
|
|
2509
|
+
const stateConfig = this.states.get(state);
|
|
2510
|
+
if (stateConfig?.history && stateConfig.history !== "deep" && stateConfig.regions) {
|
|
2511
|
+
const historyState = this.historyMap.get(state);
|
|
2512
|
+
if (historyState && adaptee) {
|
|
2513
|
+
const currentState = this.getCurrentState(adaptee) ?? "";
|
|
2514
|
+
const newCompositeState = this.updatePartialState(
|
|
2515
|
+
currentState,
|
|
2516
|
+
state,
|
|
2517
|
+
historyState
|
|
2518
|
+
);
|
|
2519
|
+
adaptee.set(
|
|
2520
|
+
this.stateAttribute,
|
|
2521
|
+
newCompositeState
|
|
2522
|
+
);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
if (stateConfig?.history === "deep" && stateConfig.regions) {
|
|
2527
|
+
const historyState = this.historyMap.get(state);
|
|
2528
|
+
if (historyState && adaptee) {
|
|
2529
|
+
adaptee.set(
|
|
2530
|
+
this.stateAttribute,
|
|
2531
|
+
historyState
|
|
2532
|
+
);
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
if (adaptee) {
|
|
2537
|
+
this.setCurrentStateInternal(state, adaptee);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
setCurrentStateInternal(state, adaptee) {
|
|
2541
|
+
const currentState = this.getCurrentState(adaptee) || "";
|
|
2542
|
+
const currentStateMap = this.parseCompositeState(currentState);
|
|
2543
|
+
const newStateParts = state.split("|");
|
|
2544
|
+
const isRootState = newStateParts.length === 1 && !state.includes(".");
|
|
2545
|
+
if (isRootState) {
|
|
2546
|
+
currentStateMap.clear();
|
|
2547
|
+
currentStateMap.set(state, state);
|
|
2548
|
+
} else {
|
|
2549
|
+
for (const newStatePart of newStateParts) {
|
|
2550
|
+
if (!this.states.has(newStatePart)) {
|
|
2551
|
+
throw new StateMachineError(
|
|
2552
|
+
`Invalid state path: ${newStatePart} in composite state: ${state} states: ${Array.from(this.states.keys()).join(",")}`,
|
|
2553
|
+
{ state: newStatePart }
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
const regionKey = this.getRegionKey(newStatePart);
|
|
2557
|
+
const stateConfig = this.states.get(newStatePart);
|
|
2558
|
+
for (const [existingRegionKey] of currentStateMap.entries()) {
|
|
2559
|
+
if (existingRegionKey === regionKey || existingRegionKey.startsWith(regionKey + ".") || regionKey.startsWith(existingRegionKey + ".")) {
|
|
2560
|
+
currentStateMap.delete(existingRegionKey);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (stateConfig?.regions) {
|
|
2564
|
+
const initialStatesForRegions = this.getInitialStatesForRegions(
|
|
2565
|
+
stateConfig.regions,
|
|
2566
|
+
newStatePart
|
|
2567
|
+
);
|
|
2568
|
+
const regionStates = initialStatesForRegions.split("|");
|
|
2569
|
+
for (const regionState of regionStates) {
|
|
2570
|
+
const regionKeyNested = this.getRegionKey(regionState);
|
|
2571
|
+
currentStateMap.set(regionKeyNested, regionState);
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
currentStateMap.set(regionKey, newStatePart);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
for (const [key, value] of currentStateMap.entries()) {
|
|
2578
|
+
if (!value.includes(".")) {
|
|
2579
|
+
currentStateMap.delete(key);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const newCompositeState = Array.from(currentStateMap.values()).join("|");
|
|
2584
|
+
this.validateCompositeState(newCompositeState);
|
|
2585
|
+
adaptee.set(
|
|
2586
|
+
this.stateAttribute,
|
|
2587
|
+
newCompositeState
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
getCurrentState(adaptee) {
|
|
2591
|
+
const targetAdaptee = adaptee || this.adaptee;
|
|
2592
|
+
if (!targetAdaptee) return;
|
|
2593
|
+
const currentState = targetAdaptee.get(this.stateAttribute);
|
|
2594
|
+
if (!currentState) return "";
|
|
2595
|
+
const stateParts = currentState.split("|");
|
|
2596
|
+
for (const statePart of stateParts) {
|
|
2597
|
+
if (!this.states.has(statePart)) {
|
|
2598
|
+
throw new StateMachineError(
|
|
2599
|
+
`Invalid state path in current state: ${statePart}`,
|
|
2600
|
+
{ state: currentState }
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return currentState;
|
|
2605
|
+
}
|
|
2606
|
+
setInitialState(initialState, obj) {
|
|
2607
|
+
const targetAdaptee = obj || this.adaptee;
|
|
2608
|
+
if (!targetAdaptee) return;
|
|
2609
|
+
const stateConfig = this.states.get(initialState);
|
|
2610
|
+
let initialStates;
|
|
2611
|
+
if (stateConfig?.regions) {
|
|
2612
|
+
initialStates = this.getInitialStatesForRegions(
|
|
2613
|
+
stateConfig.regions,
|
|
2614
|
+
initialState
|
|
2615
|
+
);
|
|
2616
|
+
} else {
|
|
2617
|
+
initialStates = initialState;
|
|
2618
|
+
}
|
|
2619
|
+
this.setCurrentState(initialStates, targetAdaptee);
|
|
2620
|
+
const stateParts = initialStates.split("|");
|
|
2621
|
+
const context = {
|
|
2622
|
+
state: initialStates,
|
|
2623
|
+
phase: "enter"
|
|
2624
|
+
};
|
|
2625
|
+
for (const statePart of stateParts) {
|
|
2626
|
+
this.executeEnterActions(
|
|
2627
|
+
targetAdaptee,
|
|
2628
|
+
statePart,
|
|
2629
|
+
[],
|
|
2630
|
+
context
|
|
2631
|
+
).catch((err) => {
|
|
2632
|
+
this.logger.error(
|
|
2633
|
+
"Error in initial state enter actions",
|
|
2634
|
+
{ state: statePart },
|
|
2635
|
+
err
|
|
2636
|
+
);
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
getInitialCompositeState(initialState) {
|
|
2641
|
+
const stateConfig = this.states.get(initialState);
|
|
2642
|
+
if (stateConfig?.regions) {
|
|
2643
|
+
return this.getInitialStatesForRegions(stateConfig.regions, initialState);
|
|
2644
|
+
}
|
|
2645
|
+
return initialState;
|
|
2646
|
+
}
|
|
2647
|
+
getDirectChildren(stateName) {
|
|
2648
|
+
const prefix = `${stateName}.`;
|
|
2649
|
+
const depth = stateName.split(".").length + 1;
|
|
2650
|
+
const children = [];
|
|
2651
|
+
for (const name of this.states.keys()) {
|
|
2652
|
+
const nameStr = String(name);
|
|
2653
|
+
if (!nameStr.startsWith(prefix)) continue;
|
|
2654
|
+
if (nameStr.split(".").length !== depth) continue;
|
|
2655
|
+
children.push(nameStr);
|
|
2656
|
+
}
|
|
2657
|
+
return children;
|
|
2658
|
+
}
|
|
2659
|
+
getInitialStatesForRegions(regions, parentPath) {
|
|
2660
|
+
const regionStates = [];
|
|
2661
|
+
for (const [regionName, regionStatesConfig] of Object.entries(regions)) {
|
|
2662
|
+
const regionPath = `${parentPath}.${regionName}`;
|
|
2663
|
+
const initialState = regionStatesConfig.initial || Object.keys(regionStatesConfig)[0];
|
|
2664
|
+
const fullPath = `${regionPath}.${initialState}`;
|
|
2665
|
+
const stateConfig = this.states.get(fullPath);
|
|
2666
|
+
if (stateConfig?.regions) {
|
|
2667
|
+
const nestedInitialStates = this.getInitialStatesForRegions(
|
|
2668
|
+
stateConfig.regions,
|
|
2669
|
+
fullPath
|
|
2670
|
+
);
|
|
2671
|
+
regionStates.push(...nestedInitialStates.split("|"));
|
|
2672
|
+
} else {
|
|
2673
|
+
regionStates.push(fullPath);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return regionStates.join("|");
|
|
2677
|
+
}
|
|
2678
|
+
validateCompositeState(compositeState) {
|
|
2679
|
+
const stateParts = compositeState.split("|");
|
|
2680
|
+
const regionKeys = /* @__PURE__ */ new Set();
|
|
2681
|
+
for (const statePart of stateParts) {
|
|
2682
|
+
const regionKey = this.getRegionKey(statePart);
|
|
2683
|
+
if (regionKeys.has(regionKey)) {
|
|
2684
|
+
throw new StateMachineError(
|
|
2685
|
+
`Contradictory state detected: multiple states for region ${regionKey} in composite state ${compositeState}`,
|
|
2686
|
+
{ state: compositeState }
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
regionKeys.add(regionKey);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
processStates(statesConfig, parentStateName) {
|
|
2693
|
+
for (const [name, value] of Object.entries(statesConfig)) {
|
|
2694
|
+
if (name === "initial") continue;
|
|
2695
|
+
const stateName = parentStateName ? `${parentStateName}.${name}` : name;
|
|
2696
|
+
const state = { name: stateName, ...value };
|
|
2697
|
+
this.states.set(stateName, state);
|
|
2698
|
+
if (value.regions) {
|
|
2699
|
+
this.processRegions(value.regions, stateName);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
processRegions(regionsConfig, parentStateName) {
|
|
2704
|
+
for (const [regionName, regionStatesConfig] of Object.entries(
|
|
2705
|
+
regionsConfig
|
|
2706
|
+
)) {
|
|
2707
|
+
this.processStates(regionStatesConfig, `${parentStateName}.${regionName}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
processError(adaptee, context, ...fallback) {
|
|
2711
|
+
let handler = (adaptee2, err) => {
|
|
2712
|
+
let currentState;
|
|
2713
|
+
try {
|
|
2714
|
+
if (adaptee2 && typeof adaptee2.get === "function") {
|
|
2715
|
+
currentState = this.getCurrentState(
|
|
2716
|
+
adaptee2
|
|
2717
|
+
) ?? "";
|
|
2718
|
+
} else {
|
|
2719
|
+
currentState = context.state || "";
|
|
2720
|
+
}
|
|
2721
|
+
} catch {
|
|
2722
|
+
currentState = context.state || "";
|
|
2723
|
+
}
|
|
2724
|
+
const _phase = err instanceof StateMachineError ? err.context.phase ?? context.phase : context.phase;
|
|
2725
|
+
const _event = err instanceof StateMachineError ? err.context.event ?? context.event : context.event;
|
|
2726
|
+
const _action = err instanceof StateMachineError ? err.context.action ?? context.action : context.action;
|
|
2727
|
+
const _transition = err instanceof StateMachineError ? err.context.transition ?? context.transition : context.transition;
|
|
2728
|
+
const errorContext = {
|
|
2729
|
+
state: currentState,
|
|
2730
|
+
..._phase !== void 0 ? { phase: _phase } : {},
|
|
2731
|
+
..._event !== void 0 ? { event: _event } : {},
|
|
2732
|
+
..._action !== void 0 ? { action: _action } : {},
|
|
2733
|
+
..._transition !== void 0 ? { transition: _transition } : {}
|
|
2734
|
+
};
|
|
2735
|
+
throw new StateMachineError(
|
|
2736
|
+
`Error in state machine: ${err ? err instanceof Error ? err.message : String(err) : "Unknown error"}`,
|
|
2737
|
+
errorContext,
|
|
2738
|
+
err instanceof StateMachineError ? err.cause : err instanceof Error ? err : void 0
|
|
2739
|
+
);
|
|
2740
|
+
};
|
|
2741
|
+
const handlers = (fallback ?? [this.onError]).filter(Boolean);
|
|
2742
|
+
if (handlers.length > 0) {
|
|
2743
|
+
const r = handlers.map(
|
|
2744
|
+
(action) => action ? typeof action === "function" ? action : this.context && this.context[action] || adaptee.get(action) : void 0
|
|
2745
|
+
).filter(Boolean).find((t) => t);
|
|
2746
|
+
if (r) handler = r;
|
|
2747
|
+
}
|
|
2748
|
+
return (...args) => {
|
|
2749
|
+
const targetAdaptee = args.length >= 2 ? args[0] : adaptee;
|
|
2750
|
+
const error = args.length >= 2 ? args[1] : args[0];
|
|
2751
|
+
return handler(targetAdaptee, error);
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
async callAction(obj, actionName, ...args) {
|
|
2755
|
+
const _callActionState = this.getCurrentState(
|
|
2756
|
+
obj
|
|
2757
|
+
);
|
|
2758
|
+
const context = {
|
|
2759
|
+
/* c8 ignore next */
|
|
2760
|
+
..._callActionState !== void 0 ? { state: _callActionState } : {},
|
|
2761
|
+
phase: "action",
|
|
2762
|
+
action: typeof actionName === "string" ? actionName : "anonymous"
|
|
2763
|
+
};
|
|
2764
|
+
const executeAction = async () => {
|
|
2765
|
+
try {
|
|
2766
|
+
if (this.context && this.context[actionName]) {
|
|
2767
|
+
const action = this.context[actionName];
|
|
2768
|
+
if (typeof action === "function") {
|
|
2769
|
+
const result = action(this.context, ...args);
|
|
2770
|
+
return result instanceof Promise ? await result : result;
|
|
2771
|
+
}
|
|
2772
|
+
} else if (typeof actionName === "function") {
|
|
2773
|
+
const result = actionName(obj, ...args);
|
|
2774
|
+
return result instanceof Promise ? await result : result;
|
|
2775
|
+
} else if (obj.get(actionName)) {
|
|
2776
|
+
const action = obj.get(actionName);
|
|
2777
|
+
if (typeof action === "function") {
|
|
2778
|
+
const result = action(obj, ...args);
|
|
2779
|
+
return result instanceof Promise ? await result : result;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
throw new StateMachineError("No action found", context);
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
if (error instanceof StateMachineError) throw error;
|
|
2785
|
+
throw new StateMachineError(
|
|
2786
|
+
`Error executing action: ${error instanceof Error ? error.message : String(error)}`,
|
|
2787
|
+
context,
|
|
2788
|
+
/* c8 ignore next */
|
|
2789
|
+
error instanceof Error ? error : void 0
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
if (this.transitionTimeout && this.transitionTimeout > 0) {
|
|
2794
|
+
return Promise.race([
|
|
2795
|
+
executeAction(),
|
|
2796
|
+
new Promise(
|
|
2797
|
+
(_, reject) => setTimeout(
|
|
2798
|
+
() => reject(new StateMachineError("Transition timeout", {
|
|
2799
|
+
/* c8 ignore next */
|
|
2800
|
+
action: typeof actionName === "string" ? actionName : "anonymous",
|
|
2801
|
+
phase: "action"
|
|
2802
|
+
})),
|
|
2803
|
+
this.transitionTimeout
|
|
2804
|
+
)
|
|
2805
|
+
)
|
|
2806
|
+
]);
|
|
2807
|
+
}
|
|
2808
|
+
return executeAction();
|
|
2809
|
+
}
|
|
2810
|
+
async getAllowedTransitions(obj, transitions, ...args) {
|
|
2811
|
+
let highestPriority = Number.NEGATIVE_INFINITY;
|
|
2812
|
+
let selectedTransition;
|
|
2813
|
+
for (const transition of transitions) {
|
|
2814
|
+
if ((transition.priority ?? Number.NEGATIVE_INFINITY) < highestPriority) {
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
const _guardState = this.getCurrentState(obj);
|
|
2818
|
+
const context = {
|
|
2819
|
+
/* c8 ignore next */
|
|
2820
|
+
..._guardState !== void 0 ? { state: _guardState } : {},
|
|
2821
|
+
phase: "guard",
|
|
2822
|
+
transition: `${transition.from} -> ${transition.to}`
|
|
2823
|
+
};
|
|
2824
|
+
const guardResult = transition.guard ? await this.callAction(obj, transition.guard, ...args).catch(
|
|
2825
|
+
this.processError(
|
|
2826
|
+
obj,
|
|
2827
|
+
context,
|
|
2828
|
+
transition.onError,
|
|
2829
|
+
this.onError
|
|
2830
|
+
)
|
|
2831
|
+
) : true;
|
|
2832
|
+
if (!guardResult) {
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
highestPriority = transition.priority ?? Number.NEGATIVE_INFINITY;
|
|
2836
|
+
selectedTransition = transition;
|
|
2837
|
+
}
|
|
2838
|
+
return selectedTransition;
|
|
2839
|
+
}
|
|
2840
|
+
isTransitionPossible(transition, currentState) {
|
|
2841
|
+
const currentStates = this.parseCompositeState(currentState);
|
|
2842
|
+
if (transition.from === "*") {
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
const fromStates = transition.from.split("|");
|
|
2846
|
+
return fromStates.every((fromState) => {
|
|
2847
|
+
if (fromState === "*") return true;
|
|
2848
|
+
const regionKey = this.getRegionKey(fromState);
|
|
2849
|
+
const currentStateForRegion = currentStates.get(regionKey);
|
|
2850
|
+
if (!currentStateForRegion) return false;
|
|
2851
|
+
return currentStateForRegion === fromState || this.isParentState(currentStateForRegion, fromState);
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
isParentState(parentState, childState) {
|
|
2855
|
+
return childState.startsWith(parentState + ".") || childState === parentState;
|
|
2856
|
+
}
|
|
2857
|
+
async applyTransition(obj, currentState, transition, args, eventName, event) {
|
|
2858
|
+
const context = {
|
|
2859
|
+
state: currentState,
|
|
2860
|
+
event: String(eventName),
|
|
2861
|
+
transition: `${transition.from} -> ${transition.to}`,
|
|
2862
|
+
phase: "transition"
|
|
2863
|
+
};
|
|
2864
|
+
const targetState = transition.to === "*" ? currentState : transition.to;
|
|
2865
|
+
this._isTransitioning = true;
|
|
2866
|
+
this._targetState = targetState;
|
|
2867
|
+
try {
|
|
2868
|
+
if (transition.guard) {
|
|
2869
|
+
const allow = await this.callAction(obj, transition.guard, ...args).catch(
|
|
2870
|
+
this.processError(
|
|
2871
|
+
obj,
|
|
2872
|
+
{ ...context, phase: "guard" },
|
|
2873
|
+
transition.onError,
|
|
2874
|
+
this.onError
|
|
2875
|
+
)
|
|
2876
|
+
);
|
|
2877
|
+
if (!allow) {
|
|
2878
|
+
return void 0;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
if (event.onBefore) {
|
|
2882
|
+
await this.callAction(obj, event.onBefore, ...args).catch(
|
|
2883
|
+
this.processError(
|
|
2884
|
+
obj,
|
|
2885
|
+
{ ...context },
|
|
2886
|
+
transition.onError,
|
|
2887
|
+
this.onError
|
|
2888
|
+
)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
try {
|
|
2892
|
+
await this.executeExitActions(obj, transition.from, args, context);
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
if (this.abortOnExitError) {
|
|
2895
|
+
this.logger.warn("Transition aborted due to onExit error", { state: currentState, error });
|
|
2896
|
+
return void 0;
|
|
2897
|
+
}
|
|
2898
|
+
throw error;
|
|
2899
|
+
}
|
|
2900
|
+
this.manageStateHistory(transition.from, currentState, obj);
|
|
2901
|
+
if (transition.onTransition) {
|
|
2902
|
+
await this.callAction(obj, transition.onTransition, ...args).catch(
|
|
2903
|
+
this.processError(
|
|
2904
|
+
obj,
|
|
2905
|
+
{ ...context },
|
|
2906
|
+
transition.onError,
|
|
2907
|
+
this.onError
|
|
2908
|
+
)
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
try {
|
|
2912
|
+
await this.executeEnterActions(obj, transition.to, args, context);
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
if (this.errorState) {
|
|
2915
|
+
this.logger.error(`Failed to enter state '${targetState}'. Fallback to error state '${this.errorState}'`, { error });
|
|
2916
|
+
const newState2 = this.updateState(currentState, this.errorState);
|
|
2917
|
+
this.setCurrentState(newState2, obj);
|
|
2918
|
+
return this.states.get(this.errorState);
|
|
2919
|
+
}
|
|
2920
|
+
throw error;
|
|
2921
|
+
}
|
|
2922
|
+
if (event.onAfter) {
|
|
2923
|
+
try {
|
|
2924
|
+
await this.callAction(obj, event.onAfter, ...args);
|
|
2925
|
+
} catch (error) {
|
|
2926
|
+
const errorHandler = this.processError(
|
|
2927
|
+
obj,
|
|
2928
|
+
{ ...context },
|
|
2929
|
+
event.onError,
|
|
2930
|
+
this.onError
|
|
2931
|
+
);
|
|
2932
|
+
errorHandler(obj, error);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
const transitionStartTime = Date.now();
|
|
2936
|
+
const newState = this.updateState(currentState, targetState);
|
|
2937
|
+
this.setCurrentState(newState, obj);
|
|
2938
|
+
const transitionTime = Date.now() - transitionStartTime;
|
|
2939
|
+
this.monitor.recordTransition(transitionTime, true);
|
|
2940
|
+
return this.states.get(targetState);
|
|
2941
|
+
} finally {
|
|
2942
|
+
this._isTransitioning = false;
|
|
2943
|
+
this._targetState = void 0;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Execute exit actions for a state
|
|
2948
|
+
*/
|
|
2949
|
+
async executeExitActions(obj, fromStateName, args, context) {
|
|
2950
|
+
const fromState = this.states.get(fromStateName);
|
|
2951
|
+
if (this.activeTimers.has(fromStateName)) {
|
|
2952
|
+
const timers = this.activeTimers.get(fromStateName) || [];
|
|
2953
|
+
for (const timerId of timers) {
|
|
2954
|
+
this.clearTimer(timerId);
|
|
2955
|
+
}
|
|
2956
|
+
this.activeTimers.delete(fromStateName);
|
|
2957
|
+
}
|
|
2958
|
+
this.stateEntryTimes.delete(fromStateName);
|
|
2959
|
+
if (!fromState) return;
|
|
2960
|
+
const exitContext = { ...context, phase: "exit" };
|
|
2961
|
+
const exitActions = [
|
|
2962
|
+
fromState.onBeforeExit,
|
|
2963
|
+
fromState.onExit,
|
|
2964
|
+
fromState.onAfterExit
|
|
2965
|
+
];
|
|
2966
|
+
for (const action of exitActions) {
|
|
2967
|
+
if (action) {
|
|
2968
|
+
await this.callAction(obj, action, ...args).catch(
|
|
2969
|
+
this.processError(obj, exitContext, fromState.onError, this.onError)
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Execute enter actions for a state
|
|
2976
|
+
*/
|
|
2977
|
+
async executeEnterActions(obj, toStateName, args, context) {
|
|
2978
|
+
const toState = this.states.get(toStateName);
|
|
2979
|
+
if (!toState) return;
|
|
2980
|
+
const enterContext = { ...context, phase: "enter" };
|
|
2981
|
+
const enterActions = [
|
|
2982
|
+
toState.onBeforeEnter,
|
|
2983
|
+
toState.onEnter,
|
|
2984
|
+
toState.onAfterEnter
|
|
2985
|
+
];
|
|
2986
|
+
for (const action of enterActions) {
|
|
2987
|
+
if (action) {
|
|
2988
|
+
await this.callAction(obj, action, ...args).catch(
|
|
2989
|
+
this.processError(obj, enterContext, toState.onError, this.onError)
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (toState.invoke && toState.invoke.length > 0) {
|
|
2994
|
+
if (!this.stateEntryTimes.has(toStateName)) {
|
|
2995
|
+
this.stateEntryTimes.set(toStateName, Date.now());
|
|
2996
|
+
}
|
|
2997
|
+
const timers = [];
|
|
2998
|
+
for (const invocation of toState.invoke) {
|
|
2999
|
+
if (invocation.cond) {
|
|
3000
|
+
try {
|
|
3001
|
+
const shouldInvoke = invocation.cond(obj.adaptee);
|
|
3002
|
+
if (!shouldInvoke) continue;
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
this.logger.error(
|
|
3005
|
+
"Error in invoke condition",
|
|
3006
|
+
{ state: toStateName },
|
|
3007
|
+
e
|
|
3008
|
+
);
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
const callback = async () => {
|
|
3013
|
+
const currentState = this.getCurrentState(obj);
|
|
3014
|
+
if (currentState?.split("|").includes(toStateName)) {
|
|
3015
|
+
try {
|
|
3016
|
+
if (invocation.action) {
|
|
3017
|
+
await this.callAction(obj, invocation.action);
|
|
3018
|
+
}
|
|
3019
|
+
this.raiseEvent(invocation.event, obj);
|
|
3020
|
+
this.scheduleProcessing();
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
this.logger.error(
|
|
3023
|
+
"Invocation error",
|
|
3024
|
+
{ state: toStateName, event: invocation.event },
|
|
3025
|
+
err
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
const timerId = this.setTimer(callback, invocation.delay);
|
|
3031
|
+
timers.push(timerId);
|
|
3032
|
+
}
|
|
3033
|
+
this.activeTimers.set(toStateName, timers);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Helper to set timer (native or scheduled)
|
|
3038
|
+
*/
|
|
3039
|
+
setTimer(callback, delay) {
|
|
3040
|
+
const scheduler = this.scheduler;
|
|
3041
|
+
if (scheduler.isActive()) {
|
|
3042
|
+
return scheduler.schedule(delay, callback);
|
|
3043
|
+
}
|
|
3044
|
+
return setTimeout(callback, delay);
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Helper to clear timer
|
|
3048
|
+
*/
|
|
3049
|
+
clearTimer(timerId) {
|
|
3050
|
+
const scheduler = this.scheduler;
|
|
3051
|
+
if (scheduler.isActive() && typeof timerId === "object" && timerId !== null && !("ref" in timerId)) {
|
|
3052
|
+
scheduler.cancel(timerId);
|
|
3053
|
+
} else {
|
|
3054
|
+
clearTimeout(timerId);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Manage state history for transitions
|
|
3059
|
+
*/
|
|
3060
|
+
manageStateHistory(fromStateName, currentState, obj) {
|
|
3061
|
+
const fromState = this.states.get(fromStateName);
|
|
3062
|
+
if (fromState?.history) {
|
|
3063
|
+
if (fromState.history === "deep") {
|
|
3064
|
+
this.historyMap.set(fromState.name, currentState);
|
|
3065
|
+
} else if (fromState.history === "shallow" && fromState.regions) {
|
|
3066
|
+
this.historyMap.set(fromState.name, this.getCurrentState(obj) ?? "");
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
const fromStateParent = this.findParentStateWithHistory(fromStateName);
|
|
3070
|
+
if (fromStateParent?.history) {
|
|
3071
|
+
this.historyMap.set(fromStateParent.name, currentState);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
findParentStateWithHistory(stateName) {
|
|
3075
|
+
let current = stateName;
|
|
3076
|
+
while (current.includes(".")) {
|
|
3077
|
+
current = current.split(".").slice(0, -1).join(".");
|
|
3078
|
+
const state = this.states.get(current);
|
|
3079
|
+
if (state?.history) return state;
|
|
3080
|
+
}
|
|
3081
|
+
return void 0;
|
|
3082
|
+
}
|
|
3083
|
+
updatePartialState(currentState, parentState, newSubstate) {
|
|
3084
|
+
if (!currentState) return newSubstate;
|
|
3085
|
+
const currentParts = currentState.split("|");
|
|
3086
|
+
const regionKey = parentState.split(".").slice(0, -1).join(".");
|
|
3087
|
+
let updatedParts = currentParts.map((part) => {
|
|
3088
|
+
const partRegionKey = this.getRegionKey(part);
|
|
3089
|
+
if (partRegionKey === regionKey) {
|
|
3090
|
+
return newSubstate;
|
|
3091
|
+
}
|
|
3092
|
+
if (partRegionKey.startsWith(`${regionKey}.`)) {
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
return part;
|
|
3096
|
+
}).filter((m) => m !== null && m !== void 0);
|
|
3097
|
+
if (!updatedParts.some((part) => this.getRegionKey(part) === regionKey)) {
|
|
3098
|
+
const regionState = this.states.get(newSubstate);
|
|
3099
|
+
if (regionState?.regions) {
|
|
3100
|
+
return currentState;
|
|
3101
|
+
} else {
|
|
3102
|
+
updatedParts.push(newSubstate);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
updatedParts = updatedParts.filter((part) => part !== null);
|
|
3106
|
+
const newCompositeState = updatedParts.join("|");
|
|
3107
|
+
this.validateCompositeState(newCompositeState);
|
|
3108
|
+
return newCompositeState;
|
|
3109
|
+
}
|
|
3110
|
+
updateState(currentState, toState) {
|
|
3111
|
+
const currentStateMap = this.parseCompositeState(currentState);
|
|
3112
|
+
const toStateParts = toState.split("|");
|
|
3113
|
+
if (toStateParts.length === 1 && !toState.includes(".")) {
|
|
3114
|
+
currentStateMap.clear();
|
|
3115
|
+
currentStateMap.set(toState, toState);
|
|
3116
|
+
return toState;
|
|
3117
|
+
}
|
|
3118
|
+
for (const toStatePart of toStateParts) {
|
|
3119
|
+
const regionKey = this.getRegionKey(toStatePart);
|
|
3120
|
+
const stateConfig = this.states.get(toStatePart);
|
|
3121
|
+
this.removeConflictingStates(currentStateMap, regionKey);
|
|
3122
|
+
if (stateConfig?.regions) {
|
|
3123
|
+
this.addRegionStates(currentStateMap, stateConfig, toStatePart);
|
|
3124
|
+
} else {
|
|
3125
|
+
currentStateMap.set(regionKey, toStatePart);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
this.cleanupRootStates(currentStateMap);
|
|
3129
|
+
const newCompositeState = Array.from(currentStateMap.values()).join("|");
|
|
3130
|
+
this.validateCompositeState(newCompositeState);
|
|
3131
|
+
return newCompositeState;
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Remove conflicting states from the state map
|
|
3135
|
+
*/
|
|
3136
|
+
removeConflictingStates(currentStateMap, regionKey) {
|
|
3137
|
+
const keysToDelete = [];
|
|
3138
|
+
for (const [key, value] of currentStateMap.entries()) {
|
|
3139
|
+
const existingRegionKey = this.getRegionKey(value);
|
|
3140
|
+
if (existingRegionKey === regionKey || existingRegionKey.startsWith(`${regionKey}.`)) {
|
|
3141
|
+
keysToDelete.push(key);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
for (const key of keysToDelete) {
|
|
3145
|
+
currentStateMap.delete(key);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Add region states to the state map
|
|
3150
|
+
*/
|
|
3151
|
+
addRegionStates(currentStateMap, stateConfig, toStatePart) {
|
|
3152
|
+
const initialStatesForRegions = this.getInitialStatesForRegions(
|
|
3153
|
+
stateConfig.regions,
|
|
3154
|
+
toStatePart
|
|
3155
|
+
);
|
|
3156
|
+
const regionStates = initialStatesForRegions.split("|");
|
|
3157
|
+
for (const regionState of regionStates) {
|
|
3158
|
+
const regionKeyNested = this.getRegionKey(regionState);
|
|
3159
|
+
currentStateMap.set(regionKeyNested, regionState);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Clean up root states from the state map
|
|
3164
|
+
*/
|
|
3165
|
+
cleanupRootStates(currentStateMap) {
|
|
3166
|
+
const keysToDelete = [];
|
|
3167
|
+
for (const [key, value] of currentStateMap.entries()) {
|
|
3168
|
+
if (!value.includes(".")) {
|
|
3169
|
+
keysToDelete.push(key);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
for (const key of keysToDelete) {
|
|
3173
|
+
currentStateMap.delete(key);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
parseCompositeState(compositeState) {
|
|
3177
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
3178
|
+
if (!compositeState) return stateMap;
|
|
3179
|
+
const stateParts = compositeState.split("|");
|
|
3180
|
+
for (let i = 0; i < stateParts.length; i++) {
|
|
3181
|
+
const statePart = stateParts[i];
|
|
3182
|
+
if (statePart === void 0) continue;
|
|
3183
|
+
const regionKey = this.getRegionKey(statePart);
|
|
3184
|
+
stateMap.set(regionKey, statePart);
|
|
3185
|
+
}
|
|
3186
|
+
return stateMap;
|
|
3187
|
+
}
|
|
3188
|
+
getRegionKey(statePath) {
|
|
3189
|
+
const lastDotIndex = statePath.lastIndexOf(".");
|
|
3190
|
+
return lastDotIndex === -1 ? statePath : statePath.substring(0, lastDotIndex);
|
|
3191
|
+
}
|
|
3192
|
+
resumeTimers() {
|
|
3193
|
+
for (const timers of this.activeTimers.values()) {
|
|
3194
|
+
for (const id of timers) {
|
|
3195
|
+
this.clearTimer(id);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
this.activeTimers.clear();
|
|
3199
|
+
const currentState = this.getCurrentState();
|
|
3200
|
+
if (!currentState) return;
|
|
3201
|
+
const activeStates = currentState.split("|");
|
|
3202
|
+
const activeStatesSet = new Set(activeStates);
|
|
3203
|
+
for (const stateName of this.stateEntryTimes.keys()) {
|
|
3204
|
+
if (!activeStatesSet.has(stateName)) {
|
|
3205
|
+
this.stateEntryTimes.delete(stateName);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
const now = Date.now();
|
|
3209
|
+
for (const stateName of activeStates) {
|
|
3210
|
+
const state = this.states.get(stateName);
|
|
3211
|
+
if (!state || !state.invoke || state.invoke.length === 0) continue;
|
|
3212
|
+
const entryTime = this.stateEntryTimes.get(stateName);
|
|
3213
|
+
const startTime = entryTime || now;
|
|
3214
|
+
if (!entryTime) {
|
|
3215
|
+
this.stateEntryTimes.set(stateName, startTime);
|
|
3216
|
+
}
|
|
3217
|
+
const elapsed = now - startTime;
|
|
3218
|
+
const timers = [];
|
|
3219
|
+
for (const invocation of state.invoke) {
|
|
3220
|
+
const remaining = Math.max(0, invocation.delay - elapsed);
|
|
3221
|
+
const callback = async () => {
|
|
3222
|
+
const current = this.getCurrentState(this.adaptee);
|
|
3223
|
+
if (current?.split("|").includes(stateName)) {
|
|
3224
|
+
try {
|
|
3225
|
+
if (invocation.cond && this.adaptee) {
|
|
3226
|
+
try {
|
|
3227
|
+
if (!invocation.cond(this.adaptee.adaptee)) return;
|
|
3228
|
+
} catch (_e) {
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (invocation.action && this.adaptee) {
|
|
3233
|
+
await this.callAction(this.adaptee, invocation.action);
|
|
3234
|
+
}
|
|
3235
|
+
await this.fireEvent(invocation.event).catch((err) => {
|
|
3236
|
+
this.logger.error(
|
|
3237
|
+
"Resumed timer transition error",
|
|
3238
|
+
{ state: stateName, event: invocation.event },
|
|
3239
|
+
err
|
|
3240
|
+
);
|
|
3241
|
+
});
|
|
3242
|
+
} catch (_err) {
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
const timerId = this.setTimer(callback, remaining);
|
|
3247
|
+
timers.push(timerId);
|
|
3248
|
+
}
|
|
3249
|
+
this.activeTimers.set(stateName, timers);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
// Методы сериализации
|
|
3253
|
+
toJSON() {
|
|
3254
|
+
const serializedStates = {};
|
|
3255
|
+
const serializedEvents = {};
|
|
3256
|
+
for (const [name, state] of this.states) {
|
|
3257
|
+
serializedStates[name] = this.serializeState(state);
|
|
3258
|
+
}
|
|
3259
|
+
for (const [name, event] of this.events) {
|
|
3260
|
+
serializedEvents[name] = this.serializeEvent(event);
|
|
3261
|
+
}
|
|
3262
|
+
const config = {
|
|
3263
|
+
initialState: this.initialState,
|
|
3264
|
+
stateAttribute: this.stateAttribute,
|
|
3265
|
+
states: serializedStates,
|
|
3266
|
+
events: serializedEvents,
|
|
3267
|
+
onError: this.serializeAction(this.onError, "config_onError")
|
|
3268
|
+
};
|
|
3269
|
+
return JSON.stringify({
|
|
3270
|
+
config,
|
|
3271
|
+
currentState: this.getCurrentState(),
|
|
3272
|
+
historyMap: Array.from(this.historyMap.entries()),
|
|
3273
|
+
stateEntryTimes: Array.from(this.stateEntryTimes.entries())
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Securely serializes the StateMachine to JSON (Async).
|
|
3278
|
+
* Generates cryptographic hashes for all functions.
|
|
3279
|
+
*/
|
|
3280
|
+
async toSecureJSON() {
|
|
3281
|
+
const serializedStates = {};
|
|
3282
|
+
const serializedEvents = {};
|
|
3283
|
+
for (const [name, state] of this.states) {
|
|
3284
|
+
serializedStates[name] = await this.serializeStateAsync(state);
|
|
3285
|
+
}
|
|
3286
|
+
for (const [name, event] of this.events) {
|
|
3287
|
+
serializedEvents[name] = await this.serializeEventAsync(event);
|
|
3288
|
+
}
|
|
3289
|
+
const config = {
|
|
3290
|
+
initialState: this.initialState,
|
|
3291
|
+
stateAttribute: this.stateAttribute,
|
|
3292
|
+
states: serializedStates,
|
|
3293
|
+
events: serializedEvents,
|
|
3294
|
+
onError: await this.serializeActionAsync(this.onError, "config_onError")
|
|
3295
|
+
};
|
|
3296
|
+
return JSON.stringify({
|
|
3297
|
+
config,
|
|
3298
|
+
currentState: this.getCurrentState(),
|
|
3299
|
+
historyMap: Array.from(this.historyMap.entries()),
|
|
3300
|
+
stateEntryTimes: Array.from(this.stateEntryTimes.entries())
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* Serialize state with optimized performance (Async)
|
|
3305
|
+
*/
|
|
3306
|
+
async serializeStateAsync(state) {
|
|
3307
|
+
const { name: _name, ...stateWithoutName } = state;
|
|
3308
|
+
const serializedState = {
|
|
3309
|
+
...stateWithoutName,
|
|
3310
|
+
onBeforeEnter: await this.serializeActionAsync(
|
|
3311
|
+
state.onBeforeEnter,
|
|
3312
|
+
"onBeforeEnter"
|
|
3313
|
+
),
|
|
3314
|
+
onEnter: await this.serializeActionAsync(state.onEnter, "onEnter"),
|
|
3315
|
+
onAfterEnter: await this.serializeActionAsync(
|
|
3316
|
+
state.onAfterEnter,
|
|
3317
|
+
"onAfterEnter"
|
|
3318
|
+
),
|
|
3319
|
+
onBeforeExit: await this.serializeActionAsync(
|
|
3320
|
+
state.onBeforeExit,
|
|
3321
|
+
"onBeforeExit"
|
|
3322
|
+
),
|
|
3323
|
+
onExit: await this.serializeActionAsync(state.onExit, "onExit"),
|
|
3324
|
+
onAfterExit: await this.serializeActionAsync(
|
|
3325
|
+
state.onAfterExit,
|
|
3326
|
+
"onAfterExit"
|
|
3327
|
+
),
|
|
3328
|
+
onError: await this.serializeActionAsync(state.onError, "state_onError")
|
|
3329
|
+
};
|
|
3330
|
+
if (state.invoke) {
|
|
3331
|
+
serializedState.invoke = await Promise.all(
|
|
3332
|
+
state.invoke.map(async (inv) => ({
|
|
3333
|
+
...inv,
|
|
3334
|
+
cond: inv.cond != null ? await safeFunctionSerializer.serializeActionAsync(
|
|
3335
|
+
inv.cond,
|
|
3336
|
+
"invoke_cond"
|
|
3337
|
+
) : void 0,
|
|
3338
|
+
action: inv.action != null ? await safeFunctionSerializer.serializeActionAsync(
|
|
3339
|
+
inv.action,
|
|
3340
|
+
"invoke_action"
|
|
3341
|
+
) : void 0
|
|
3342
|
+
}))
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
return serializedState;
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Serialize state with optimized performance
|
|
3349
|
+
*/
|
|
3350
|
+
serializeState(state) {
|
|
3351
|
+
const { name: _name, ...stateWithoutName } = state;
|
|
3352
|
+
const serializedState = {
|
|
3353
|
+
...stateWithoutName,
|
|
3354
|
+
onBeforeEnter: this.serializeAction(state.onBeforeEnter, "onBeforeEnter"),
|
|
3355
|
+
onEnter: this.serializeAction(state.onEnter, "onEnter"),
|
|
3356
|
+
onAfterEnter: this.serializeAction(state.onAfterEnter, "onAfterEnter"),
|
|
3357
|
+
onBeforeExit: this.serializeAction(state.onBeforeExit, "onBeforeExit"),
|
|
3358
|
+
onExit: this.serializeAction(state.onExit, "onExit"),
|
|
3359
|
+
onAfterExit: this.serializeAction(state.onAfterExit, "onAfterExit"),
|
|
3360
|
+
onError: this.serializeAction(state.onError, "state_onError")
|
|
3361
|
+
};
|
|
3362
|
+
if (state.invoke) {
|
|
3363
|
+
serializedState.invoke = state.invoke.map((inv) => ({
|
|
3364
|
+
...inv,
|
|
3365
|
+
cond: inv.cond != null ? safeFunctionSerializer.serializeAction(inv.cond, "invoke_cond") : void 0,
|
|
3366
|
+
action: inv.action != null ? safeFunctionSerializer.serializeAction(inv.action, "invoke_action") : void 0
|
|
3367
|
+
}));
|
|
3368
|
+
}
|
|
3369
|
+
return serializedState;
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Serialize event with optimized performance (Async)
|
|
3373
|
+
*/
|
|
3374
|
+
async serializeEventAsync(event) {
|
|
3375
|
+
const { name: _name, ...eventWithoutName } = event;
|
|
3376
|
+
return {
|
|
3377
|
+
...eventWithoutName,
|
|
3378
|
+
onBefore: await this.serializeActionAsync(
|
|
3379
|
+
event.onBefore,
|
|
3380
|
+
"event_onBefore"
|
|
3381
|
+
),
|
|
3382
|
+
onAfter: await this.serializeActionAsync(event.onAfter, "event_onAfter"),
|
|
3383
|
+
onSuccess: await this.serializeActionAsync(
|
|
3384
|
+
event.onSuccess,
|
|
3385
|
+
"event_onSuccess"
|
|
3386
|
+
),
|
|
3387
|
+
onError: await this.serializeActionAsync(event.onError, "event_onError"),
|
|
3388
|
+
transitions: await Promise.all(
|
|
3389
|
+
event.transitions.map((t) => this.serializeTransitionAsync(t))
|
|
3390
|
+
)
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Serialize transition (Async)
|
|
3395
|
+
*/
|
|
3396
|
+
async serializeTransitionAsync(transition) {
|
|
3397
|
+
return {
|
|
3398
|
+
...transition,
|
|
3399
|
+
guard: await this.serializeActionAsync(
|
|
3400
|
+
transition.guard,
|
|
3401
|
+
"transition_guard"
|
|
3402
|
+
),
|
|
3403
|
+
onTransition: await this.serializeActionAsync(
|
|
3404
|
+
transition.onTransition,
|
|
3405
|
+
"transition_onTransition"
|
|
3406
|
+
),
|
|
3407
|
+
onError: await this.serializeActionAsync(
|
|
3408
|
+
transition.onError,
|
|
3409
|
+
"transition_onError"
|
|
3410
|
+
)
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Serialize event with optimized performance
|
|
3415
|
+
*/
|
|
3416
|
+
serializeEvent(event) {
|
|
3417
|
+
const { name: _name, ...eventWithoutName } = event;
|
|
3418
|
+
return {
|
|
3419
|
+
...eventWithoutName,
|
|
3420
|
+
onBefore: this.serializeAction(event.onBefore, "event_onBefore"),
|
|
3421
|
+
onAfter: this.serializeAction(event.onAfter, "event_onAfter"),
|
|
3422
|
+
onSuccess: this.serializeAction(event.onSuccess, "event_onSuccess"),
|
|
3423
|
+
onError: this.serializeAction(event.onError, "event_onError"),
|
|
3424
|
+
transitions: event.transitions.map((transition) => ({
|
|
3425
|
+
...transition,
|
|
3426
|
+
guard: this.serializeAction(transition.guard, "transition_guard"),
|
|
3427
|
+
onTransition: this.serializeAction(
|
|
3428
|
+
transition.onTransition,
|
|
3429
|
+
"transition_onTransition"
|
|
3430
|
+
),
|
|
3431
|
+
onError: this.serializeAction(transition.onError, "transition_onError")
|
|
3432
|
+
}))
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
async serializeActionAsync(action, name) {
|
|
3436
|
+
if (!action) return void 0;
|
|
3437
|
+
return safeFunctionSerializer.serializeActionAsync(action, name);
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Optimized action serialization using safe serializer
|
|
3441
|
+
*/
|
|
3442
|
+
serializeAction(action, functionName) {
|
|
3443
|
+
if (typeof action === "function") {
|
|
3444
|
+
return safeFunctionSerializer.serializeAction(action, functionName);
|
|
3445
|
+
}
|
|
3446
|
+
return action;
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
|
|
3450
|
+
// src/lite.ts
|
|
3451
|
+
function createMachine(config, owner, options) {
|
|
3452
|
+
if (owner && "get" in owner) {
|
|
3453
|
+
return new StateMachine(config, owner, options);
|
|
3454
|
+
} else if (owner) {
|
|
3455
|
+
const adapter = new MemoryAdapter(owner);
|
|
3456
|
+
return new StateMachine(config, adapter, options);
|
|
3457
|
+
} else {
|
|
3458
|
+
return new StateMachine(config, void 0, options);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
// src/config_validator.ts
|
|
3463
|
+
var DEFAULT_VALIDATION_CONFIG = {
|
|
3464
|
+
strictMode: false,
|
|
3465
|
+
allowEmptyStates: false,
|
|
3466
|
+
allowEmptyEvents: false,
|
|
3467
|
+
maxStateDepth: 10,
|
|
3468
|
+
maxStatesCount: 1e3,
|
|
3469
|
+
maxEventsCount: 1e3,
|
|
3470
|
+
requireInitialState: true,
|
|
3471
|
+
validateTransitionPaths: true,
|
|
3472
|
+
validateActionReferences: false
|
|
3473
|
+
// Disabled by default as actions might be dynamic
|
|
3474
|
+
};
|
|
3475
|
+
var ConfigValidator = class {
|
|
3476
|
+
config;
|
|
3477
|
+
errors = [];
|
|
3478
|
+
warnings = [];
|
|
3479
|
+
constructor(config = {}) {
|
|
3480
|
+
this.config = { ...DEFAULT_VALIDATION_CONFIG, ...config };
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Validates a complete StateMachine configuration
|
|
3484
|
+
*/
|
|
3485
|
+
validate(smConfig) {
|
|
3486
|
+
this.errors = [];
|
|
3487
|
+
this.warnings = [];
|
|
3488
|
+
try {
|
|
3489
|
+
this.validateBasicStructure(smConfig);
|
|
3490
|
+
this.validateStates(smConfig.states, "states");
|
|
3491
|
+
this.validateEvents(smConfig.events, smConfig.states, "events");
|
|
3492
|
+
this.validateInitialState(smConfig);
|
|
3493
|
+
this.validateCrossReferences(smConfig);
|
|
3494
|
+
this.validatePerformanceConstraints(smConfig);
|
|
3495
|
+
this.runCustomRules(smConfig);
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3498
|
+
this.addError(
|
|
3499
|
+
"VALIDATION_FAILED",
|
|
3500
|
+
`Validation process failed: ${msg}`,
|
|
3501
|
+
"validation"
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
const result = {
|
|
3505
|
+
isValid: this.errors.length === 0,
|
|
3506
|
+
errors: this.errors,
|
|
3507
|
+
warnings: this.warnings
|
|
3508
|
+
};
|
|
3509
|
+
if (!result.isValid) {
|
|
3510
|
+
stateMachineLogger.error("StateMachine configuration validation failed", {
|
|
3511
|
+
errorCount: this.errors.length,
|
|
3512
|
+
warningCount: this.warnings.length,
|
|
3513
|
+
configName: smConfig.name
|
|
3514
|
+
});
|
|
3515
|
+
} else if (this.warnings.length > 0) {
|
|
3516
|
+
stateMachineLogger.warn("StateMachine configuration has warnings", {
|
|
3517
|
+
warningCount: this.warnings.length,
|
|
3518
|
+
configName: smConfig.name
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
return result;
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Runs custom validation rules
|
|
3525
|
+
*/
|
|
3526
|
+
runCustomRules(smConfig) {
|
|
3527
|
+
if (!this.config.customRules || this.config.customRules.length === 0) return;
|
|
3528
|
+
const context = {
|
|
3529
|
+
addError: (code, msg, path, sugg) => this.addError(code, msg, path, void 0, sugg),
|
|
3530
|
+
addWarning: (code, msg, path, sugg) => this.addWarning(code, msg, path, void 0, sugg)
|
|
3531
|
+
};
|
|
3532
|
+
for (const rule of this.config.customRules) {
|
|
3533
|
+
try {
|
|
3534
|
+
rule.validate(smConfig, context);
|
|
3535
|
+
} catch (e) {
|
|
3536
|
+
this.addError(
|
|
3537
|
+
"CUSTOM_RULE_ERROR",
|
|
3538
|
+
`Rule ${rule.id} failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
3539
|
+
"root"
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
validateBasicStructure(smConfig) {
|
|
3545
|
+
if (!smConfig.name || typeof smConfig.name !== "string") {
|
|
3546
|
+
this.addError(
|
|
3547
|
+
"MISSING_NAME",
|
|
3548
|
+
"StateMachine must have a valid name",
|
|
3549
|
+
"name"
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
if (!smConfig.stateAttribute || typeof smConfig.stateAttribute !== "string") {
|
|
3553
|
+
this.addError(
|
|
3554
|
+
"MISSING_STATE_ATTRIBUTE",
|
|
3555
|
+
"StateMachine must have a valid stateAttribute",
|
|
3556
|
+
"stateAttribute"
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
if (!smConfig.states || typeof smConfig.states !== "object") {
|
|
3560
|
+
this.addError(
|
|
3561
|
+
"MISSING_STATES",
|
|
3562
|
+
"StateMachine must have states configuration",
|
|
3563
|
+
"states"
|
|
3564
|
+
);
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
if (!smConfig.events || typeof smConfig.events !== "object") {
|
|
3568
|
+
this.addError(
|
|
3569
|
+
"MISSING_EVENTS",
|
|
3570
|
+
"StateMachine must have events configuration",
|
|
3571
|
+
"events"
|
|
3572
|
+
);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
if (!this.config.allowEmptyStates && Object.keys(smConfig.states).length === 0) {
|
|
3576
|
+
this.addError(
|
|
3577
|
+
"EMPTY_STATES",
|
|
3578
|
+
"StateMachine cannot have empty states collection",
|
|
3579
|
+
"states"
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
if (!this.config.allowEmptyEvents && Object.keys(smConfig.events).length === 0) {
|
|
3583
|
+
this.addError(
|
|
3584
|
+
"EMPTY_EVENTS",
|
|
3585
|
+
"StateMachine cannot have empty events collection",
|
|
3586
|
+
"events"
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
validateStates(states, basePath, depth = 0) {
|
|
3591
|
+
if (depth > this.config.maxStateDepth) {
|
|
3592
|
+
this.addError(
|
|
3593
|
+
"MAX_DEPTH_EXCEEDED",
|
|
3594
|
+
`State nesting depth exceeds maximum of ${this.config.maxStateDepth}`,
|
|
3595
|
+
basePath
|
|
3596
|
+
);
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
for (const [stateName, stateConfig] of Object.entries(states)) {
|
|
3600
|
+
const statePath = `${basePath}.${stateName}`;
|
|
3601
|
+
this.validateState(stateConfig, statePath, depth);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
validateState(state, path, depth) {
|
|
3605
|
+
if (!path.split(".").pop()) {
|
|
3606
|
+
this.addError("INVALID_STATE_NAME", "State name cannot be empty", path);
|
|
3607
|
+
}
|
|
3608
|
+
if (state.display && typeof state.display !== "string") {
|
|
3609
|
+
this.addWarning(
|
|
3610
|
+
"INVALID_DISPLAY",
|
|
3611
|
+
"State display should be a string",
|
|
3612
|
+
`${path}.display`
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
if (state.regions) {
|
|
3616
|
+
if (typeof state.regions !== "object") {
|
|
3617
|
+
this.addError(
|
|
3618
|
+
"INVALID_REGIONS",
|
|
3619
|
+
"State regions must be an object",
|
|
3620
|
+
`${path}.regions`
|
|
3621
|
+
);
|
|
3622
|
+
} else {
|
|
3623
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
3624
|
+
state.regions
|
|
3625
|
+
)) {
|
|
3626
|
+
if (!regionName) {
|
|
3627
|
+
this.addError(
|
|
3628
|
+
"EMPTY_REGION_NAME",
|
|
3629
|
+
"Region name cannot be empty",
|
|
3630
|
+
`${path}.regions`
|
|
3631
|
+
);
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
this.validateStates(
|
|
3635
|
+
regionStates,
|
|
3636
|
+
`${path}.regions.${regionName}`,
|
|
3637
|
+
depth + 1
|
|
3638
|
+
);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
if (state.history && !["deep", "shallow"].includes(state.history)) {
|
|
3643
|
+
this.addError(
|
|
3644
|
+
"INVALID_HISTORY",
|
|
3645
|
+
'State history must be "deep" or "shallow"',
|
|
3646
|
+
`${path}.history`
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
if (state.regions && state.initial) {
|
|
3650
|
+
const hasInitialState = Object.values(state.regions).some(
|
|
3651
|
+
(regionStates) => Object.keys(regionStates).includes(state.initial)
|
|
3652
|
+
);
|
|
3653
|
+
if (!hasInitialState) {
|
|
3654
|
+
this.addError(
|
|
3655
|
+
"INVALID_INITIAL_STATE",
|
|
3656
|
+
`Initial state "${state.initial}" not found in any region`,
|
|
3657
|
+
`${path}.initial`
|
|
3658
|
+
);
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
if (state.invoke) {
|
|
3662
|
+
if (!Array.isArray(state.invoke)) {
|
|
3663
|
+
this.addError(
|
|
3664
|
+
"INVALID_INVOKE",
|
|
3665
|
+
'State "invoke" must be an array of StateInvocation',
|
|
3666
|
+
`${path}.invoke`
|
|
3667
|
+
);
|
|
3668
|
+
} else {
|
|
3669
|
+
state.invoke.forEach((inv, index) => {
|
|
3670
|
+
const invPath = `${path}.invoke[${index}]`;
|
|
3671
|
+
if (typeof inv.delay !== "number" || inv.delay < 0) {
|
|
3672
|
+
this.addError(
|
|
3673
|
+
"INVALID_DELAY",
|
|
3674
|
+
"Delay must be a positive number",
|
|
3675
|
+
`${invPath}.delay`
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
if (!inv.event || typeof inv.event !== "string") {
|
|
3679
|
+
this.addError(
|
|
3680
|
+
"INVALID_EVENT_NAME",
|
|
3681
|
+
"Event name must be a string",
|
|
3682
|
+
`${invPath}.event`
|
|
3683
|
+
);
|
|
3684
|
+
}
|
|
3685
|
+
if (inv.cond && typeof inv.cond !== "function") {
|
|
3686
|
+
this.addError(
|
|
3687
|
+
"INVALID_COND",
|
|
3688
|
+
"Condition must be a function",
|
|
3689
|
+
`${invPath}.cond`
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
validateEvents(events, states, basePath) {
|
|
3697
|
+
for (const [eventName, eventConfig] of Object.entries(events)) {
|
|
3698
|
+
const eventPath = `${basePath}.${eventName}`;
|
|
3699
|
+
this.validateEvent(eventConfig, eventPath, states);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
validateEvent(event, path, states) {
|
|
3703
|
+
if (!event.transitions || !Array.isArray(event.transitions)) {
|
|
3704
|
+
this.addError(
|
|
3705
|
+
"MISSING_TRANSITIONS",
|
|
3706
|
+
"Event must have transitions array",
|
|
3707
|
+
`${path}.transitions`
|
|
3708
|
+
);
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
if (event.transitions.length === 0) {
|
|
3712
|
+
this.addWarning(
|
|
3713
|
+
"EMPTY_TRANSITIONS",
|
|
3714
|
+
"Event has no transitions",
|
|
3715
|
+
`${path}.transitions`
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
event.transitions.forEach((transition, index) => {
|
|
3719
|
+
this.validateTransition(
|
|
3720
|
+
transition,
|
|
3721
|
+
`${path}.transitions[${index}]`,
|
|
3722
|
+
states
|
|
3723
|
+
);
|
|
3724
|
+
});
|
|
3725
|
+
const priorities = event.transitions.map((t) => t.priority).filter((p) => p !== void 0);
|
|
3726
|
+
if (priorities.length > 0 && priorities.length !== event.transitions.length) {
|
|
3727
|
+
this.addWarning(
|
|
3728
|
+
"MIXED_PRIORITIES",
|
|
3729
|
+
"Some transitions have priority while others do not",
|
|
3730
|
+
`${path}.transitions`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
validateTransition(transition, path, states) {
|
|
3735
|
+
if (!transition.from || typeof transition.from !== "string") {
|
|
3736
|
+
this.addError(
|
|
3737
|
+
"INVALID_FROM_STATE",
|
|
3738
|
+
"Transition must have a valid from state",
|
|
3739
|
+
`${path}.from`
|
|
3740
|
+
);
|
|
3741
|
+
} else if (this.config.validateTransitionPaths) {
|
|
3742
|
+
this.validateStatePath(transition.from, states, `${path}.from`);
|
|
3743
|
+
}
|
|
3744
|
+
if (!transition.to || typeof transition.to !== "string") {
|
|
3745
|
+
this.addError(
|
|
3746
|
+
"INVALID_TO_STATE",
|
|
3747
|
+
"Transition must have a valid to state",
|
|
3748
|
+
`${path}.to`
|
|
3749
|
+
);
|
|
3750
|
+
} else if (this.config.validateTransitionPaths) {
|
|
3751
|
+
this.validateStatePath(transition.to, states, `${path}.to`);
|
|
3752
|
+
}
|
|
3753
|
+
if (transition.priority !== void 0 && typeof transition.priority !== "number") {
|
|
3754
|
+
this.addError(
|
|
3755
|
+
"INVALID_PRIORITY",
|
|
3756
|
+
"Transition priority must be a number",
|
|
3757
|
+
`${path}.priority`
|
|
3758
|
+
);
|
|
3759
|
+
}
|
|
3760
|
+
if (transition.from === transition.to) {
|
|
3761
|
+
this.addWarning(
|
|
3762
|
+
"SELF_TRANSITION",
|
|
3763
|
+
"Transition from and to states are the same",
|
|
3764
|
+
path
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
validateStatePath(statePath, states, path) {
|
|
3769
|
+
const stateParts = statePath.split("|");
|
|
3770
|
+
for (const part of stateParts) {
|
|
3771
|
+
if (!this.isValidStatePath(part, states)) {
|
|
3772
|
+
this.addError(
|
|
3773
|
+
"INVALID_STATE_PATH",
|
|
3774
|
+
`State path "${part}" does not exist`,
|
|
3775
|
+
path
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
isValidStatePath(statePath, states) {
|
|
3781
|
+
const parts = statePath.split(".");
|
|
3782
|
+
let currentStates = states;
|
|
3783
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3784
|
+
const part = parts[i];
|
|
3785
|
+
if (part === void 0) return false;
|
|
3786
|
+
if (i === 0) {
|
|
3787
|
+
if (!currentStates[part]) {
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
const state = currentStates[part];
|
|
3791
|
+
if (parts.length === 1) {
|
|
3792
|
+
return true;
|
|
3793
|
+
}
|
|
3794
|
+
if (!state.regions) {
|
|
3795
|
+
return false;
|
|
3796
|
+
}
|
|
3797
|
+
} else if (i === 1) {
|
|
3798
|
+
const rootStatePart = parts[0];
|
|
3799
|
+
if (rootStatePart === void 0) return false;
|
|
3800
|
+
const rootState = currentStates[rootStatePart];
|
|
3801
|
+
if (!rootState || !rootState.regions || !rootState.regions[part]) {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
currentStates = rootState.regions[part];
|
|
3805
|
+
} else {
|
|
3806
|
+
if (!currentStates[part]) {
|
|
3807
|
+
return false;
|
|
3808
|
+
}
|
|
3809
|
+
const state = currentStates[part];
|
|
3810
|
+
if (i === parts.length - 1) {
|
|
3811
|
+
return true;
|
|
3812
|
+
}
|
|
3813
|
+
if (!state.regions) {
|
|
3814
|
+
return false;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
return true;
|
|
3819
|
+
}
|
|
3820
|
+
validateInitialState(smConfig) {
|
|
3821
|
+
if (this.config.requireInitialState) {
|
|
3822
|
+
if (!smConfig.initialState) {
|
|
3823
|
+
this.addError(
|
|
3824
|
+
"MISSING_INITIAL_STATE",
|
|
3825
|
+
"StateMachine must have an initial state",
|
|
3826
|
+
"initialState"
|
|
3827
|
+
);
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
if (!smConfig.states[smConfig.initialState]) {
|
|
3831
|
+
this.addError(
|
|
3832
|
+
"INVALID_INITIAL_STATE",
|
|
3833
|
+
`Initial state "${smConfig.initialState}" does not exist`,
|
|
3834
|
+
"initialState",
|
|
3835
|
+
void 0,
|
|
3836
|
+
`Check the 'states' object. Ensure that '${smConfig.initialState}' is defined as a root state.`
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
validateCrossReferences(smConfig) {
|
|
3842
|
+
const allStateNames = /* @__PURE__ */ new Set();
|
|
3843
|
+
this.collectStateNames(smConfig.states, "", allStateNames);
|
|
3844
|
+
const reachableStates = /* @__PURE__ */ new Set();
|
|
3845
|
+
if (smConfig.initialState) {
|
|
3846
|
+
reachableStates.add(smConfig.initialState);
|
|
3847
|
+
}
|
|
3848
|
+
for (const event of Object.values(smConfig.events)) {
|
|
3849
|
+
for (const transition of event.transitions) {
|
|
3850
|
+
if (transition.to) {
|
|
3851
|
+
const toParts = transition.to.split("|");
|
|
3852
|
+
toParts.forEach((part) => reachableStates.add(part));
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
for (const stateName of allStateNames) {
|
|
3857
|
+
if (!reachableStates.has(stateName)) {
|
|
3858
|
+
this.addWarning(
|
|
3859
|
+
"UNREACHABLE_STATE",
|
|
3860
|
+
`State "${stateName}" is not reachable`,
|
|
3861
|
+
`states.${stateName}`
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
for (const [eventName, event] of Object.entries(smConfig.events)) {
|
|
3866
|
+
const hasValidTransitions = event.transitions.some(
|
|
3867
|
+
(t) => allStateNames.has(t.from) && allStateNames.has(t.to)
|
|
3868
|
+
);
|
|
3869
|
+
if (!hasValidTransitions) {
|
|
3870
|
+
this.addWarning(
|
|
3871
|
+
"UNUSED_EVENT",
|
|
3872
|
+
`Event "${eventName}" has no valid transitions`,
|
|
3873
|
+
`events.${eventName}`
|
|
3874
|
+
);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
collectStateNames(states, prefix, collector) {
|
|
3879
|
+
for (const [stateName, state] of Object.entries(states)) {
|
|
3880
|
+
const fullName = prefix ? `${prefix}.${stateName}` : stateName;
|
|
3881
|
+
collector.add(fullName);
|
|
3882
|
+
if (state.regions) {
|
|
3883
|
+
for (const [regionName, regionStates] of Object.entries(
|
|
3884
|
+
state.regions
|
|
3885
|
+
)) {
|
|
3886
|
+
this.collectStateNames(
|
|
3887
|
+
regionStates,
|
|
3888
|
+
`${fullName}.${regionName}`,
|
|
3889
|
+
collector
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
validatePerformanceConstraints(smConfig) {
|
|
3896
|
+
const stateCount = this.countStates(smConfig.states);
|
|
3897
|
+
const eventCount = Object.keys(smConfig.events).length;
|
|
3898
|
+
if (stateCount > this.config.maxStatesCount) {
|
|
3899
|
+
this.addWarning(
|
|
3900
|
+
"TOO_MANY_STATES",
|
|
3901
|
+
`State count (${stateCount}) exceeds recommended maximum (${this.config.maxStatesCount})`,
|
|
3902
|
+
"states"
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
if (eventCount > this.config.maxEventsCount) {
|
|
3906
|
+
this.addWarning(
|
|
3907
|
+
"TOO_MANY_EVENTS",
|
|
3908
|
+
`Event count (${eventCount}) exceeds recommended maximum (${this.config.maxEventsCount})`,
|
|
3909
|
+
"events"
|
|
3910
|
+
);
|
|
3911
|
+
}
|
|
3912
|
+
let totalTransitions = 0;
|
|
3913
|
+
for (const event of Object.values(smConfig.events)) {
|
|
3914
|
+
totalTransitions += event.transitions.length;
|
|
3915
|
+
}
|
|
3916
|
+
if (totalTransitions > stateCount * 3) {
|
|
3917
|
+
this.addWarning(
|
|
3918
|
+
"COMPLEX_TRANSITIONS",
|
|
3919
|
+
"High transition-to-state ratio may impact performance",
|
|
3920
|
+
"events"
|
|
3921
|
+
);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
countStates(states) {
|
|
3925
|
+
let count = Object.keys(states).length;
|
|
3926
|
+
for (const state of Object.values(states)) {
|
|
3927
|
+
if (state.regions) {
|
|
3928
|
+
for (const regionStates of Object.values(state.regions)) {
|
|
3929
|
+
count += this.countStates(regionStates);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
return count;
|
|
3934
|
+
}
|
|
3935
|
+
addError(code, message, path, details, suggestion) {
|
|
3936
|
+
this.errors.push({
|
|
3937
|
+
code,
|
|
3938
|
+
message,
|
|
3939
|
+
severity: "error",
|
|
3940
|
+
path,
|
|
3941
|
+
...details !== void 0 ? { details } : {},
|
|
3942
|
+
...suggestion !== void 0 ? { suggestion } : {}
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
addWarning(code, message, path, details, suggestion) {
|
|
3946
|
+
this.warnings.push({
|
|
3947
|
+
code,
|
|
3948
|
+
message,
|
|
3949
|
+
severity: "warning",
|
|
3950
|
+
path,
|
|
3951
|
+
...details !== void 0 ? { details } : {},
|
|
3952
|
+
...suggestion !== void 0 ? { suggestion } : {}
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
function validateConfig(config, validationConfig) {
|
|
3957
|
+
const validator = new ConfigValidator(validationConfig);
|
|
3958
|
+
return validator.validate(config);
|
|
3959
|
+
}
|
|
3960
|
+
function validateConfigStrict(config) {
|
|
3961
|
+
return validateConfig(config, { strictMode: true });
|
|
3962
|
+
}
|
|
3963
|
+
function isValidConfig(config) {
|
|
3964
|
+
return validateConfig(config).isValid;
|
|
3965
|
+
}
|
|
3966
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3967
|
+
0 && (module.exports = {
|
|
3968
|
+
EnhancedErrorHandler,
|
|
3969
|
+
EnhancedStateMachineError,
|
|
3970
|
+
ErrorAnalytics,
|
|
3971
|
+
ErrorCategory,
|
|
3972
|
+
ErrorSeverity,
|
|
3973
|
+
FallbackStateRecoveryStrategy,
|
|
3974
|
+
LocalStorageAdapter,
|
|
3975
|
+
MemoryAdapter,
|
|
3976
|
+
RetryRecoveryStrategy,
|
|
3977
|
+
ServerAdapter,
|
|
3978
|
+
SessionStorageAdapter,
|
|
3979
|
+
StateMachine,
|
|
3980
|
+
StateMachineError,
|
|
3981
|
+
createEnhancedError,
|
|
3982
|
+
createMachine,
|
|
3983
|
+
isAdapter,
|
|
3984
|
+
isRecoverableError,
|
|
3985
|
+
isValidConfig,
|
|
3986
|
+
validateConfig,
|
|
3987
|
+
validateConfigStrict
|
|
3988
|
+
});
|
|
3989
|
+
//# sourceMappingURL=index.cjs.map
|