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.
Files changed (37) hide show
  1. package/README.md +9 -9
  2. package/dist/s3db.cjs.js +3637 -191
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +3637 -191
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/clients/memory-client.class.js +16 -16
  8. package/src/clients/s3-client.class.js +17 -17
  9. package/src/concerns/error-classifier.js +204 -0
  10. package/src/database.class.js +9 -9
  11. package/src/plugins/api/index.js +1 -7
  12. package/src/plugins/api/routes/resource-routes.js +3 -3
  13. package/src/plugins/api/server.js +29 -9
  14. package/src/plugins/audit.plugin.js +2 -4
  15. package/src/plugins/backup.plugin.js +10 -12
  16. package/src/plugins/cache.plugin.js +4 -6
  17. package/src/plugins/concerns/plugin-dependencies.js +12 -0
  18. package/src/plugins/costs.plugin.js +0 -2
  19. package/src/plugins/eventual-consistency/index.js +1 -3
  20. package/src/plugins/fulltext.plugin.js +2 -4
  21. package/src/plugins/geo.plugin.js +3 -5
  22. package/src/plugins/importer/index.js +0 -2
  23. package/src/plugins/index.js +0 -1
  24. package/src/plugins/metrics.plugin.js +2 -4
  25. package/src/plugins/ml.plugin.js +1004 -42
  26. package/src/plugins/plugin.class.js +1 -3
  27. package/src/plugins/queue-consumer.plugin.js +1 -3
  28. package/src/plugins/relation.plugin.js +2 -4
  29. package/src/plugins/replicator.plugin.js +18 -20
  30. package/src/plugins/s3-queue.plugin.js +6 -8
  31. package/src/plugins/scheduler.plugin.js +9 -11
  32. package/src/plugins/state-machine.errors.js +9 -1
  33. package/src/plugins/state-machine.plugin.js +605 -20
  34. package/src/plugins/tfstate/index.js +0 -2
  35. package/src/plugins/ttl.plugin.js +40 -25
  36. package/src/plugins/vector.plugin.js +10 -12
  37. 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
- this.emit('initialized', { machines: Array.from(this.machines.keys()) });
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
- const [ok, error] = await tryFn(() =>
326
- action(context, event, { database: this.database, machineId, entityId })
327
- );
328
-
329
- if (!ok) {
330
- if (this.config.verbose) {
331
- console.error(`[StateMachinePlugin] Action '${actionName}' failed:`, error.message);
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('entity_initialized', { machineId, entityId, initialState });
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
+ }