s3db.js 13.0.0 → 13.2.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/README.md +9 -9
- package/dist/s3db.cjs.js +3637 -191
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +3637 -191
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/clients/memory-client.class.js +16 -16
- package/src/clients/s3-client.class.js +17 -17
- package/src/concerns/error-classifier.js +204 -0
- package/src/database.class.js +9 -9
- package/src/plugins/api/index.js +1 -7
- package/src/plugins/api/routes/resource-routes.js +3 -3
- package/src/plugins/api/server.js +29 -9
- package/src/plugins/audit.plugin.js +2 -4
- package/src/plugins/backup.plugin.js +10 -12
- package/src/plugins/cache.plugin.js +4 -6
- package/src/plugins/concerns/plugin-dependencies.js +12 -0
- package/src/plugins/costs.plugin.js +0 -2
- package/src/plugins/eventual-consistency/index.js +1 -3
- package/src/plugins/fulltext.plugin.js +2 -4
- package/src/plugins/geo.plugin.js +3 -5
- package/src/plugins/importer/index.js +0 -2
- package/src/plugins/index.js +0 -1
- package/src/plugins/metrics.plugin.js +2 -4
- package/src/plugins/ml.plugin.js +1004 -42
- package/src/plugins/plugin.class.js +1 -3
- package/src/plugins/queue-consumer.plugin.js +1 -3
- package/src/plugins/relation.plugin.js +2 -4
- package/src/plugins/replicator.plugin.js +18 -20
- package/src/plugins/s3-queue.plugin.js +6 -8
- package/src/plugins/scheduler.plugin.js +9 -11
- package/src/plugins/state-machine.errors.js +9 -1
- package/src/plugins/state-machine.plugin.js +605 -20
- package/src/plugins/tfstate/index.js +0 -2
- package/src/plugins/ttl.plugin.js +40 -25
- package/src/plugins/vector.plugin.js +10 -12
- package/src/resource.class.js +58 -40
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import Plugin from "./plugin.class.js";
|
|
1
|
+
import { Plugin } from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
3
|
import { StateMachineError } from "./state-machine.errors.js";
|
|
4
|
+
import { ErrorClassifier } from "../concerns/error-classifier.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* StateMachinePlugin - Finite State Machine Management
|
|
@@ -114,11 +115,24 @@ export class StateMachinePlugin extends Plugin {
|
|
|
114
115
|
// Distributed lock configuration (prevents concurrent transitions)
|
|
115
116
|
workerId: options.workerId || 'default',
|
|
116
117
|
lockTimeout: options.lockTimeout || 1000, // Wait up to 1s for lock
|
|
117
|
-
lockTTL: options.lockTTL || 5 // Lock expires after 5s (prevent deadlock)
|
|
118
|
+
lockTTL: options.lockTTL || 5, // Lock expires after 5s (prevent deadlock)
|
|
119
|
+
|
|
120
|
+
// Global retry configuration for action execution
|
|
121
|
+
retryConfig: options.retryConfig || null,
|
|
122
|
+
|
|
123
|
+
// Trigger system configuration
|
|
124
|
+
enableScheduler: options.enableScheduler || false,
|
|
125
|
+
schedulerConfig: options.schedulerConfig || {},
|
|
126
|
+
enableDateTriggers: options.enableDateTriggers !== false,
|
|
127
|
+
enableFunctionTriggers: options.enableFunctionTriggers !== false,
|
|
128
|
+
enableEventTriggers: options.enableEventTriggers !== false,
|
|
129
|
+
triggerCheckInterval: options.triggerCheckInterval || 60000 // Check triggers every 60s by default
|
|
118
130
|
};
|
|
119
131
|
|
|
120
132
|
this.database = null;
|
|
121
133
|
this.machines = new Map();
|
|
134
|
+
this.triggerIntervals = [];
|
|
135
|
+
this.schedulerPlugin = null;
|
|
122
136
|
|
|
123
137
|
this._validateConfiguration();
|
|
124
138
|
}
|
|
@@ -163,12 +177,12 @@ export class StateMachinePlugin extends Plugin {
|
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
async onInstall() {
|
|
166
|
-
|
|
180
|
+
|
|
167
181
|
// Create state storage resource if persistence is enabled
|
|
168
182
|
if (this.config.persistTransitions) {
|
|
169
183
|
await this._createStateResources();
|
|
170
184
|
}
|
|
171
|
-
|
|
185
|
+
|
|
172
186
|
// Initialize state machines
|
|
173
187
|
for (const [machineName, machineConfig] of Object.entries(this.config.stateMachines)) {
|
|
174
188
|
this.machines.set(machineName, {
|
|
@@ -176,8 +190,11 @@ export class StateMachinePlugin extends Plugin {
|
|
|
176
190
|
currentStates: new Map() // entityId -> currentState
|
|
177
191
|
});
|
|
178
192
|
}
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
|
|
194
|
+
// Setup trigger system if enabled
|
|
195
|
+
await this._setupTriggers();
|
|
196
|
+
|
|
197
|
+
this.emit('db:plugin:initialized', { machines: Array.from(this.machines.keys()) });
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
async _createStateResources() {
|
|
@@ -212,6 +229,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
212
229
|
currentState: 'string|required',
|
|
213
230
|
context: 'json|default:{}',
|
|
214
231
|
lastTransition: 'string|default:null',
|
|
232
|
+
triggerCounts: 'json|default:{}', // Track trigger execution counts
|
|
215
233
|
updatedAt: 'string|required'
|
|
216
234
|
},
|
|
217
235
|
behavior: 'body-overflow'
|
|
@@ -292,7 +310,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
292
310
|
await this._executeAction(targetStateConfig.entry, context, event, machineId, entityId);
|
|
293
311
|
}
|
|
294
312
|
|
|
295
|
-
this.emit('transition', {
|
|
313
|
+
this.emit('plg:state-machine:transition', {
|
|
296
314
|
machineId,
|
|
297
315
|
entityId,
|
|
298
316
|
from: currentState,
|
|
@@ -321,16 +339,132 @@ export class StateMachinePlugin extends Plugin {
|
|
|
321
339
|
}
|
|
322
340
|
return;
|
|
323
341
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
|
|
343
|
+
// Get retry configuration (state-specific overrides global)
|
|
344
|
+
const machine = this.machines.get(machineId);
|
|
345
|
+
const currentState = await this.getState(machineId, entityId);
|
|
346
|
+
const stateConfig = machine?.config?.states?.[currentState];
|
|
347
|
+
|
|
348
|
+
// Merge retry configs: global < machine < state
|
|
349
|
+
const retryConfig = {
|
|
350
|
+
...(this.config.retryConfig || {}),
|
|
351
|
+
...(machine?.config?.retryConfig || {}),
|
|
352
|
+
...(stateConfig?.retryConfig || {})
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const maxAttempts = retryConfig.maxAttempts ?? 0;
|
|
356
|
+
const retryEnabled = maxAttempts > 0;
|
|
357
|
+
let attempt = 0;
|
|
358
|
+
let lastError = null;
|
|
359
|
+
|
|
360
|
+
while (attempt <= maxAttempts) {
|
|
361
|
+
try {
|
|
362
|
+
const result = await action(context, event, { database: this.database, machineId, entityId });
|
|
363
|
+
|
|
364
|
+
// Success - log retry statistics if retried
|
|
365
|
+
if (attempt > 0) {
|
|
366
|
+
this.emit('plg:state-machine:action-retry-success', {
|
|
367
|
+
machineId,
|
|
368
|
+
entityId,
|
|
369
|
+
action: actionName,
|
|
370
|
+
attempts: attempt + 1,
|
|
371
|
+
state: currentState
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (this.config.verbose) {
|
|
375
|
+
console.log(`[StateMachinePlugin] Action '${actionName}' succeeded after ${attempt + 1} attempts`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return result;
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
lastError = error;
|
|
383
|
+
|
|
384
|
+
// If retries are disabled, use old behavior (emit error but don't throw)
|
|
385
|
+
if (!retryEnabled) {
|
|
386
|
+
if (this.config.verbose) {
|
|
387
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed:`, error.message);
|
|
388
|
+
}
|
|
389
|
+
this.emit('plg:state-machine:action-error', { actionName, error: error.message, machineId, entityId });
|
|
390
|
+
return; // Don't throw, continue execution
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Classify error
|
|
394
|
+
const classification = ErrorClassifier.classify(error, {
|
|
395
|
+
retryableErrors: retryConfig.retryableErrors,
|
|
396
|
+
nonRetriableErrors: retryConfig.nonRetriableErrors
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Non-retriable error - fail immediately
|
|
400
|
+
if (classification === 'NON_RETRIABLE') {
|
|
401
|
+
this.emit('plg:state-machine:action-error-non-retriable', {
|
|
402
|
+
machineId,
|
|
403
|
+
entityId,
|
|
404
|
+
action: actionName,
|
|
405
|
+
error: error.message,
|
|
406
|
+
state: currentState
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (this.config.verbose) {
|
|
410
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed with non-retriable error:`, error.message);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Max attempts reached
|
|
417
|
+
if (attempt >= maxAttempts) {
|
|
418
|
+
this.emit('plg:state-machine:action-retry-exhausted', {
|
|
419
|
+
machineId,
|
|
420
|
+
entityId,
|
|
421
|
+
action: actionName,
|
|
422
|
+
attempts: attempt + 1,
|
|
423
|
+
error: error.message,
|
|
424
|
+
state: currentState
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (this.config.verbose) {
|
|
428
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed after ${attempt + 1} attempts:`, error.message);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Retriable error - retry
|
|
435
|
+
attempt++;
|
|
436
|
+
|
|
437
|
+
// Calculate backoff delay
|
|
438
|
+
const delay = this._calculateBackoff(attempt, retryConfig);
|
|
439
|
+
|
|
440
|
+
// Call retry hook if configured
|
|
441
|
+
if (retryConfig.onRetry) {
|
|
442
|
+
try {
|
|
443
|
+
await retryConfig.onRetry(attempt, error, context);
|
|
444
|
+
} catch (hookError) {
|
|
445
|
+
if (this.config.verbose) {
|
|
446
|
+
console.warn(`[StateMachinePlugin] onRetry hook failed:`, hookError.message);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.emit('plg:state-machine:action-retry-attempt', {
|
|
452
|
+
machineId,
|
|
453
|
+
entityId,
|
|
454
|
+
action: actionName,
|
|
455
|
+
attempt,
|
|
456
|
+
delay,
|
|
457
|
+
error: error.message,
|
|
458
|
+
state: currentState
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (this.config.verbose) {
|
|
462
|
+
console.warn(`[StateMachinePlugin] Action '${actionName}' failed (attempt ${attempt + 1}/${maxAttempts + 1}), retrying in ${delay}ms:`, error.message);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Wait before retry
|
|
466
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
332
467
|
}
|
|
333
|
-
this.emit('action_error', { actionName, error: error.message, machineId, entityId });
|
|
334
468
|
}
|
|
335
469
|
}
|
|
336
470
|
|
|
@@ -453,6 +587,35 @@ export class StateMachinePlugin extends Plugin {
|
|
|
453
587
|
}
|
|
454
588
|
}
|
|
455
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Calculate backoff delay for retry attempts
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
_calculateBackoff(attempt, retryConfig) {
|
|
595
|
+
const {
|
|
596
|
+
backoffStrategy = 'exponential',
|
|
597
|
+
baseDelay = 1000,
|
|
598
|
+
maxDelay = 30000
|
|
599
|
+
} = retryConfig || {};
|
|
600
|
+
|
|
601
|
+
let delay;
|
|
602
|
+
|
|
603
|
+
if (backoffStrategy === 'exponential') {
|
|
604
|
+
// Exponential backoff: baseDelay * 2^(attempt-1)
|
|
605
|
+
delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
606
|
+
} else if (backoffStrategy === 'linear') {
|
|
607
|
+
// Linear backoff: baseDelay * attempt
|
|
608
|
+
delay = Math.min(baseDelay * attempt, maxDelay);
|
|
609
|
+
} else {
|
|
610
|
+
// Fixed backoff: always use baseDelay
|
|
611
|
+
delay = baseDelay;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Add jitter (±20%) to prevent thundering herd
|
|
615
|
+
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
|
616
|
+
return Math.round(delay + jitter);
|
|
617
|
+
}
|
|
618
|
+
|
|
456
619
|
/**
|
|
457
620
|
* Get current state for an entity
|
|
458
621
|
*/
|
|
@@ -611,7 +774,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
611
774
|
await this._executeAction(initialStateConfig.entry, context, 'INIT', machineId, entityId);
|
|
612
775
|
}
|
|
613
776
|
|
|
614
|
-
this.emit('
|
|
777
|
+
this.emit('plg:state-machine:entity-initialized', { machineId, entityId, initialState });
|
|
615
778
|
|
|
616
779
|
return initialState;
|
|
617
780
|
}
|
|
@@ -674,6 +837,418 @@ export class StateMachinePlugin extends Plugin {
|
|
|
674
837
|
return dot;
|
|
675
838
|
}
|
|
676
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Get all entities currently in a specific state
|
|
842
|
+
* @private
|
|
843
|
+
*/
|
|
844
|
+
async _getEntitiesInState(machineId, stateName) {
|
|
845
|
+
if (!this.config.persistTransitions) {
|
|
846
|
+
// Memory-only - check in-memory map
|
|
847
|
+
const machine = this.machines.get(machineId);
|
|
848
|
+
if (!machine) return [];
|
|
849
|
+
|
|
850
|
+
const entities = [];
|
|
851
|
+
for (const [entityId, currentState] of machine.currentStates) {
|
|
852
|
+
if (currentState === stateName) {
|
|
853
|
+
entities.push({ entityId, currentState, context: {}, triggerCounts: {} });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return entities;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Query state resource for entities in this state
|
|
860
|
+
const [ok, err, records] = await tryFn(() =>
|
|
861
|
+
this.database.resources[this.config.stateResource].query({
|
|
862
|
+
machineId,
|
|
863
|
+
currentState: stateName
|
|
864
|
+
})
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
if (!ok) {
|
|
868
|
+
if (this.config.verbose) {
|
|
869
|
+
console.warn(`[StateMachinePlugin] Failed to query entities in state '${stateName}':`, err.message);
|
|
870
|
+
}
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return records || [];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Increment trigger execution count for an entity
|
|
879
|
+
* @private
|
|
880
|
+
*/
|
|
881
|
+
async _incrementTriggerCount(machineId, entityId, triggerName) {
|
|
882
|
+
if (!this.config.persistTransitions) {
|
|
883
|
+
// No persistence - skip tracking
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const stateId = `${machineId}_${entityId}`;
|
|
888
|
+
|
|
889
|
+
const [ok, err, stateRecord] = await tryFn(() =>
|
|
890
|
+
this.database.resources[this.config.stateResource].get(stateId)
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
if (ok && stateRecord) {
|
|
894
|
+
const triggerCounts = stateRecord.triggerCounts || {};
|
|
895
|
+
triggerCounts[triggerName] = (triggerCounts[triggerName] || 0) + 1;
|
|
896
|
+
|
|
897
|
+
await tryFn(() =>
|
|
898
|
+
this.database.resources[this.config.stateResource].patch(stateId, { triggerCounts })
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Setup trigger system for all state machines
|
|
905
|
+
* @private
|
|
906
|
+
*/
|
|
907
|
+
async _setupTriggers() {
|
|
908
|
+
if (!this.config.enableScheduler && !this.config.enableDateTriggers && !this.config.enableFunctionTriggers && !this.config.enableEventTriggers) {
|
|
909
|
+
// All triggers disabled
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const cronJobs = {};
|
|
914
|
+
|
|
915
|
+
for (const [machineId, machineData] of this.machines) {
|
|
916
|
+
const machineConfig = machineData.config;
|
|
917
|
+
|
|
918
|
+
for (const [stateName, stateConfig] of Object.entries(machineConfig.states)) {
|
|
919
|
+
const triggers = stateConfig.triggers || [];
|
|
920
|
+
|
|
921
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
922
|
+
const trigger = triggers[i];
|
|
923
|
+
const triggerName = `${trigger.action}_${i}`;
|
|
924
|
+
|
|
925
|
+
if (trigger.type === 'cron' && this.config.enableScheduler) {
|
|
926
|
+
// Collect cron triggers for SchedulerPlugin
|
|
927
|
+
const jobName = `${machineId}_${stateName}_${triggerName}`;
|
|
928
|
+
cronJobs[jobName] = await this._createCronJob(machineId, stateName, trigger, triggerName);
|
|
929
|
+
} else if (trigger.type === 'date' && this.config.enableDateTriggers) {
|
|
930
|
+
// Setup date-based trigger
|
|
931
|
+
await this._setupDateTrigger(machineId, stateName, trigger, triggerName);
|
|
932
|
+
} else if (trigger.type === 'function' && this.config.enableFunctionTriggers) {
|
|
933
|
+
// Setup function-based trigger
|
|
934
|
+
await this._setupFunctionTrigger(machineId, stateName, trigger, triggerName);
|
|
935
|
+
} else if (trigger.type === 'event' && this.config.enableEventTriggers) {
|
|
936
|
+
// Setup event-based trigger
|
|
937
|
+
await this._setupEventTrigger(machineId, stateName, trigger, triggerName);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Install SchedulerPlugin if there are cron jobs
|
|
944
|
+
if (Object.keys(cronJobs).length > 0 && this.config.enableScheduler) {
|
|
945
|
+
const { SchedulerPlugin } = await import('./scheduler.plugin.js');
|
|
946
|
+
this.schedulerPlugin = new SchedulerPlugin({
|
|
947
|
+
jobs: cronJobs,
|
|
948
|
+
persistJobs: false, // Don't persist trigger jobs
|
|
949
|
+
verbose: this.config.verbose,
|
|
950
|
+
...this.config.schedulerConfig
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
await this.database.usePlugin(this.schedulerPlugin);
|
|
954
|
+
|
|
955
|
+
if (this.config.verbose) {
|
|
956
|
+
console.log(`[StateMachinePlugin] Installed SchedulerPlugin with ${Object.keys(cronJobs).length} cron triggers`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Create a SchedulerPlugin job for a cron trigger
|
|
963
|
+
* @private
|
|
964
|
+
*/
|
|
965
|
+
async _createCronJob(machineId, stateName, trigger, triggerName) {
|
|
966
|
+
return {
|
|
967
|
+
schedule: trigger.schedule,
|
|
968
|
+
description: `Trigger '${triggerName}' for ${machineId}.${stateName}`,
|
|
969
|
+
action: async (database, context) => {
|
|
970
|
+
// Find all entities in this state
|
|
971
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
972
|
+
|
|
973
|
+
let executedCount = 0;
|
|
974
|
+
|
|
975
|
+
for (const entity of entities) {
|
|
976
|
+
try {
|
|
977
|
+
// Check condition if provided
|
|
978
|
+
if (trigger.condition) {
|
|
979
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
980
|
+
if (!shouldTrigger) continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Check max triggers
|
|
984
|
+
if (trigger.maxTriggers !== undefined) {
|
|
985
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
986
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
987
|
+
// Send max triggers event if configured
|
|
988
|
+
if (trigger.onMaxTriggersReached) {
|
|
989
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Execute trigger action
|
|
996
|
+
const result = await this._executeAction(
|
|
997
|
+
trigger.action,
|
|
998
|
+
entity.context,
|
|
999
|
+
'TRIGGER',
|
|
1000
|
+
machineId,
|
|
1001
|
+
entity.entityId
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
// Increment trigger count
|
|
1005
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
1006
|
+
executedCount++;
|
|
1007
|
+
|
|
1008
|
+
// Send success event if configured
|
|
1009
|
+
if (trigger.eventOnSuccess) {
|
|
1010
|
+
await this.send(machineId, entity.entityId, trigger.eventOnSuccess, {
|
|
1011
|
+
...entity.context,
|
|
1012
|
+
triggerResult: result
|
|
1013
|
+
});
|
|
1014
|
+
} else if (trigger.event) {
|
|
1015
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
1016
|
+
...entity.context,
|
|
1017
|
+
triggerResult: result
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
this.emit('plg:state-machine:trigger-executed', {
|
|
1022
|
+
machineId,
|
|
1023
|
+
entityId: entity.entityId,
|
|
1024
|
+
state: stateName,
|
|
1025
|
+
trigger: triggerName,
|
|
1026
|
+
type: 'cron'
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
// Send failure event if configured
|
|
1031
|
+
if (trigger.event) {
|
|
1032
|
+
await tryFn(() => this.send(machineId, entity.entityId, trigger.event, {
|
|
1033
|
+
...entity.context,
|
|
1034
|
+
triggerError: error.message
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (this.config.verbose) {
|
|
1039
|
+
console.error(`[StateMachinePlugin] Trigger '${triggerName}' failed for entity ${entity.entityId}:`, error.message);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return { processed: entities.length, executed: executedCount };
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Setup a date-based trigger
|
|
1051
|
+
* @private
|
|
1052
|
+
*/
|
|
1053
|
+
async _setupDateTrigger(machineId, stateName, trigger, triggerName) {
|
|
1054
|
+
// Poll for entities approaching trigger date
|
|
1055
|
+
const checkInterval = setInterval(async () => {
|
|
1056
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
1057
|
+
|
|
1058
|
+
for (const entity of entities) {
|
|
1059
|
+
try {
|
|
1060
|
+
// Get trigger date from context field
|
|
1061
|
+
const triggerDateValue = entity.context?.[trigger.field];
|
|
1062
|
+
if (!triggerDateValue) continue;
|
|
1063
|
+
|
|
1064
|
+
const triggerDate = new Date(triggerDateValue);
|
|
1065
|
+
const now = new Date();
|
|
1066
|
+
|
|
1067
|
+
// Check if trigger date reached
|
|
1068
|
+
if (now >= triggerDate) {
|
|
1069
|
+
// Check max triggers
|
|
1070
|
+
if (trigger.maxTriggers !== undefined) {
|
|
1071
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
1072
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
1073
|
+
if (trigger.onMaxTriggersReached) {
|
|
1074
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
1075
|
+
}
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Execute action
|
|
1081
|
+
const result = await this._executeAction(trigger.action, entity.context, 'TRIGGER', machineId, entity.entityId);
|
|
1082
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
1083
|
+
|
|
1084
|
+
// Send event
|
|
1085
|
+
if (trigger.event) {
|
|
1086
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
1087
|
+
...entity.context,
|
|
1088
|
+
triggerResult: result
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.emit('plg:state-machine:trigger-executed', {
|
|
1093
|
+
machineId,
|
|
1094
|
+
entityId: entity.entityId,
|
|
1095
|
+
state: stateName,
|
|
1096
|
+
trigger: triggerName,
|
|
1097
|
+
type: 'date'
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (this.config.verbose) {
|
|
1102
|
+
console.error(`[StateMachinePlugin] Date trigger '${triggerName}' failed:`, error.message);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}, this.config.triggerCheckInterval);
|
|
1107
|
+
|
|
1108
|
+
this.triggerIntervals.push(checkInterval);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Setup a function-based trigger
|
|
1113
|
+
* @private
|
|
1114
|
+
*/
|
|
1115
|
+
async _setupFunctionTrigger(machineId, stateName, trigger, triggerName) {
|
|
1116
|
+
const interval = trigger.interval || this.config.triggerCheckInterval;
|
|
1117
|
+
|
|
1118
|
+
const checkInterval = setInterval(async () => {
|
|
1119
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
1120
|
+
|
|
1121
|
+
for (const entity of entities) {
|
|
1122
|
+
try {
|
|
1123
|
+
// Check max triggers
|
|
1124
|
+
if (trigger.maxTriggers !== undefined) {
|
|
1125
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
1126
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
1127
|
+
if (trigger.onMaxTriggersReached) {
|
|
1128
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
1129
|
+
}
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Evaluate condition
|
|
1135
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
1136
|
+
|
|
1137
|
+
if (shouldTrigger) {
|
|
1138
|
+
const result = await this._executeAction(trigger.action, entity.context, 'TRIGGER', machineId, entity.entityId);
|
|
1139
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
1140
|
+
|
|
1141
|
+
// Send event if configured
|
|
1142
|
+
if (trigger.event) {
|
|
1143
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
1144
|
+
...entity.context,
|
|
1145
|
+
triggerResult: result
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
this.emit('plg:state-machine:trigger-executed', {
|
|
1150
|
+
machineId,
|
|
1151
|
+
entityId: entity.entityId,
|
|
1152
|
+
state: stateName,
|
|
1153
|
+
trigger: triggerName,
|
|
1154
|
+
type: 'function'
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
if (this.config.verbose) {
|
|
1159
|
+
console.error(`[StateMachinePlugin] Function trigger '${triggerName}' failed:`, error.message);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}, interval);
|
|
1164
|
+
|
|
1165
|
+
this.triggerIntervals.push(checkInterval);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Setup an event-based trigger
|
|
1170
|
+
* @private
|
|
1171
|
+
*/
|
|
1172
|
+
async _setupEventTrigger(machineId, stateName, trigger, triggerName) {
|
|
1173
|
+
const eventName = trigger.event;
|
|
1174
|
+
|
|
1175
|
+
// Create event listener
|
|
1176
|
+
const eventHandler = async (eventData) => {
|
|
1177
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
1178
|
+
|
|
1179
|
+
for (const entity of entities) {
|
|
1180
|
+
try {
|
|
1181
|
+
// Check condition if provided
|
|
1182
|
+
if (trigger.condition) {
|
|
1183
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId, eventData);
|
|
1184
|
+
if (!shouldTrigger) continue;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Check max triggers
|
|
1188
|
+
if (trigger.maxTriggers !== undefined) {
|
|
1189
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
1190
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
1191
|
+
if (trigger.onMaxTriggersReached) {
|
|
1192
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
1193
|
+
}
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Execute trigger action with event data in context
|
|
1199
|
+
const result = await this._executeAction(
|
|
1200
|
+
trigger.action,
|
|
1201
|
+
{ ...entity.context, eventData },
|
|
1202
|
+
'TRIGGER',
|
|
1203
|
+
machineId,
|
|
1204
|
+
entity.entityId
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
1208
|
+
|
|
1209
|
+
// Send success event if configured
|
|
1210
|
+
if (trigger.sendEvent) {
|
|
1211
|
+
await this.send(machineId, entity.entityId, trigger.sendEvent, {
|
|
1212
|
+
...entity.context,
|
|
1213
|
+
triggerResult: result,
|
|
1214
|
+
eventData
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
this.emit('plg:state-machine:trigger-executed', {
|
|
1219
|
+
machineId,
|
|
1220
|
+
entityId: entity.entityId,
|
|
1221
|
+
state: stateName,
|
|
1222
|
+
trigger: triggerName,
|
|
1223
|
+
type: 'event',
|
|
1224
|
+
eventName
|
|
1225
|
+
});
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
if (this.config.verbose) {
|
|
1228
|
+
console.error(`[StateMachinePlugin] Event trigger '${triggerName}' failed:`, error.message);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// Listen to database events if eventName starts with 'db:'
|
|
1235
|
+
if (eventName.startsWith('db:')) {
|
|
1236
|
+
const dbEventName = eventName.substring(3); // Remove 'db:' prefix
|
|
1237
|
+
this.database.on(dbEventName, eventHandler);
|
|
1238
|
+
|
|
1239
|
+
if (this.config.verbose) {
|
|
1240
|
+
console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
// Listen to plugin events
|
|
1244
|
+
this.on(eventName, eventHandler);
|
|
1245
|
+
|
|
1246
|
+
if (this.config.verbose) {
|
|
1247
|
+
console.log(`[StateMachinePlugin] Listening to plugin event '${eventName}' for trigger '${triggerName}'`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
677
1252
|
async start() {
|
|
678
1253
|
if (this.config.verbose) {
|
|
679
1254
|
console.log(`[StateMachinePlugin] Started with ${this.machines.size} state machines`);
|
|
@@ -681,9 +1256,19 @@ export class StateMachinePlugin extends Plugin {
|
|
|
681
1256
|
}
|
|
682
1257
|
|
|
683
1258
|
async stop() {
|
|
1259
|
+
// Clear trigger intervals
|
|
1260
|
+
for (const interval of this.triggerIntervals) {
|
|
1261
|
+
clearInterval(interval);
|
|
1262
|
+
}
|
|
1263
|
+
this.triggerIntervals = [];
|
|
1264
|
+
|
|
1265
|
+
// Stop scheduler plugin if installed
|
|
1266
|
+
if (this.schedulerPlugin) {
|
|
1267
|
+
await this.schedulerPlugin.stop();
|
|
1268
|
+
this.schedulerPlugin = null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
684
1271
|
this.machines.clear();
|
|
685
1272
|
this.removeAllListeners();
|
|
686
1273
|
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export default StateMachinePlugin;
|
|
1274
|
+
}
|