@nocobase/plugin-workflow 2.2.0-beta.15 → 2.2.0-beta.17

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 (40) hide show
  1. package/dist/client/{67.56a3d15a55ab7d5c.js → 67.92c849b5dbe07c33.js} +1 -1
  2. package/dist/client/WorkflowTasks.d.ts +26 -3
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client-v2/index.js +1 -1
  6. package/dist/common/collections/jobs.js +2 -2
  7. package/dist/common/collections/userWorkflowTaskStats.d.ts +54 -0
  8. package/dist/common/collections/userWorkflowTaskStats.js +87 -0
  9. package/dist/externalVersion.js +13 -13
  10. package/dist/locale/en-US.json +4 -0
  11. package/dist/locale/zh-CN.json +4 -0
  12. package/dist/node_modules/cron-parser/package.json +1 -1
  13. package/dist/node_modules/joi/package.json +1 -1
  14. package/dist/node_modules/lru-cache/package.json +1 -1
  15. package/dist/node_modules/nodejs-snowflake/package.json +1 -1
  16. package/dist/server/Dispatcher.d.ts +24 -13
  17. package/dist/server/Dispatcher.js +160 -143
  18. package/dist/server/Plugin.d.ts +58 -12
  19. package/dist/server/Plugin.js +356 -16
  20. package/dist/server/Processor.d.ts +2 -7
  21. package/dist/server/Processor.js +37 -18
  22. package/dist/server/RunningExecutionRegistry.d.ts +2 -2
  23. package/dist/server/RunningExecutionRegistry.js +21 -6
  24. package/dist/server/actions/executions.js +3 -6
  25. package/dist/server/actions/index.js +3 -1
  26. package/dist/server/actions/jobs.js +1 -2
  27. package/dist/server/{migrations/20260713000000-remove-workflow-validation.d.ts → actions/userWorkflowTaskStats.d.ts} +2 -5
  28. package/dist/server/actions/userWorkflowTaskStats.js +112 -0
  29. package/dist/server/actions/workflows.js +18 -1
  30. package/dist/server/collections/userWorkflowTaskStats.d.ts +11 -0
  31. package/dist/server/collections/userWorkflowTaskStats.js +43 -0
  32. package/dist/server/index.d.ts +1 -0
  33. package/dist/server/types/Execution.d.ts +2 -0
  34. package/dist/server/types/Job.d.ts +6 -0
  35. package/dist/server/utils.d.ts +1 -0
  36. package/dist/server/utils.js +5 -0
  37. package/dist/swagger/index.d.ts +68 -1
  38. package/dist/swagger/index.js +61 -1
  39. package/package.json +2 -2
  40. package/dist/server/migrations/20260713000000-remove-workflow-validation.js +0 -56
@@ -42,6 +42,7 @@ __export(Plugin_exports, {
42
42
  module.exports = __toCommonJS(Plugin_exports);
43
43
  var import_path = __toESM(require("path"));
44
44
  var import_nodejs_snowflake = require("nodejs-snowflake");
45
+ var import_sequelize = require("sequelize");
45
46
  var import_lru_cache = require("lru-cache");
46
47
  var import_database = require("@nocobase/database");
47
48
  var import_server = require("@nocobase/server");
@@ -53,6 +54,7 @@ var import_RunningExecutionRegistry = __toESM(require("./RunningExecutionRegistr
53
54
  var import_actions = __toESM(require("./actions"));
54
55
  var import_nodes = require("./actions/nodes");
55
56
  var import_workflows = require("./actions/workflows");
57
+ var import_constants = require("./constants");
56
58
  var import_functions = __toESM(require("./functions"));
57
59
  var import_CollectionTrigger = __toESM(require("./triggers/CollectionTrigger"));
58
60
  var import_ScheduleTrigger = __toESM(require("./triggers/ScheduleTrigger"));
@@ -82,6 +84,7 @@ class PluginWorkflowServer extends import_server.Plugin {
82
84
  loggerCache;
83
85
  meter = null;
84
86
  checker = null;
87
+ taskStatsProviders = /* @__PURE__ */ new Map();
85
88
  onBeforeSave = async (instance, { transaction, cycling }) => {
86
89
  if (cycling) {
87
90
  return;
@@ -170,16 +173,16 @@ class PluginWorkflowServer extends import_server.Plugin {
170
173
  }
171
174
  }
172
175
  this.checker = setInterval(() => {
173
- this.dispatcher.dispatch();
176
+ this.dispatcher.recover({ gracePeriod: 6e4 });
174
177
  }, 3e5);
175
178
  await this.timeoutManager.load();
176
- this.app.on("workflow:dispatch", () => {
177
- this.app.logger.info("workflow:dispatch");
178
- this.dispatcher.dispatch();
179
+ this.app.on("workflow:recover", () => {
180
+ this.app.logger.info("workflow:recover");
181
+ this.dispatcher.recover();
179
182
  });
180
183
  this.dispatcher.setReady(true);
181
184
  this.getLogger("dispatcher").info("(starting) check for queueing executions");
182
- this.dispatcher.dispatch();
185
+ this.dispatcher.recover();
183
186
  };
184
187
  onBeforeStop = async () => {
185
188
  if (this.checker) {
@@ -191,7 +194,6 @@ class PluginWorkflowServer extends import_server.Plugin {
191
194
  for (const workflow of this.enabledCache.values()) {
192
195
  this.toggle(workflow, false, { silent: true });
193
196
  }
194
- this.app.eventQueue.unsubscribe(this.channelPendingExecution);
195
197
  this.loggerCache.clear();
196
198
  };
197
199
  async handleSyncMessage(message) {
@@ -223,7 +225,7 @@ class PluginWorkflowServer extends import_server.Plugin {
223
225
  return this.timeoutManager.abortExecutionIfExpired(execution, options);
224
226
  }
225
227
  registerRunningExecution(executionId, abort) {
226
- this.runningExecutionRegistry.register(executionId, { abort });
228
+ return this.runningExecutionRegistry.register(executionId, { abort });
227
229
  }
228
230
  unregisterRunningExecution(executionId) {
229
231
  this.runningExecutionRegistry.unregister(executionId);
@@ -393,6 +395,7 @@ class PluginWorkflowServer extends import_server.Plugin {
393
395
  actions: ["workflows:list"]
394
396
  });
395
397
  this.app.acl.allow("userWorkflowTasks", "listMine", "loggedIn");
398
+ this.app.acl.allow("userWorkflowTaskStats", "listMine", "loggedIn");
396
399
  db.on("workflows.beforeSave", this.onBeforeSave);
397
400
  db.on("workflows.afterCreate", this.onAfterCreate);
398
401
  db.on("workflows.afterUpdate", this.onAfterUpdate);
@@ -401,7 +404,7 @@ class PluginWorkflowServer extends import_server.Plugin {
401
404
  this.app.on("beforeStop", this.onBeforeStop);
402
405
  this.app.eventQueue.subscribe(this.channelPendingExecution, {
403
406
  idle: () => this.serving() && this.dispatcher.idle,
404
- process: this.dispatcher.onQueueExecution
407
+ process: this.dispatcher.onQueueTask
405
408
  });
406
409
  }
407
410
  toggle(workflow, enable, { silent, transaction } = {}) {
@@ -448,25 +451,70 @@ class PluginWorkflowServer extends import_server.Plugin {
448
451
  trigger(workflow, context, options = {}) {
449
452
  return this.dispatcher.trigger(workflow, context, options);
450
453
  }
451
- async run(pending) {
452
- return this.dispatcher.run(pending);
454
+ async rerun(execution, rerun) {
455
+ return this.dispatcher.enqueue({ executionId: this.getModelId(execution), rerun: rerun ?? {} });
453
456
  }
454
- dispatch() {
455
- return this.dispatcher.dispatch();
457
+ recover() {
458
+ return this.dispatcher.recover();
456
459
  }
460
+ /**
461
+ * Persist the current job state and publish a poke that evaluates that state.
462
+ *
463
+ * PENDING resume calls are valid for instructions such as delay and sequential approval.
464
+ * Callers that aggregate multiple user actions must only call this method when their
465
+ * atomic update changes the job from PENDING to a terminal status.
466
+ * Persistence and queue publication failures are logged and rethrown to awaiting callers.
467
+ */
457
468
  async resume(job) {
458
- return this.dispatcher.resume(job);
469
+ var _a;
470
+ const executionId = job.executionId ?? ((_a = job.execution) == null ? void 0 : _a.id);
471
+ if (executionId == null) {
472
+ this.getLogger("dispatcher").warn(`execution id of job (${job.id}) not found, resume ignored`);
473
+ return;
474
+ }
475
+ try {
476
+ if (job.changed()) {
477
+ await job.save();
478
+ }
479
+ } catch (error) {
480
+ this.getLogger("dispatcher").error(
481
+ `persisting job (${job.id}) before resuming execution (${executionId}) failed`,
482
+ { error, executionId, jobId: job.id }
483
+ );
484
+ throw error;
485
+ }
486
+ try {
487
+ await this.dispatcher.enqueue({ executionId, jobId: job.id });
488
+ this.getLogger("dispatcher").info(`execution (${executionId}) resuming from job (${job.id}) published to queue`);
489
+ } catch (error) {
490
+ this.getLogger("dispatcher").error(
491
+ `publishing resume task for execution (${executionId}) from job (${job.id}) failed`,
492
+ { error, executionId, jobId: job.id }
493
+ );
494
+ throw error;
495
+ }
459
496
  }
460
497
  /**
461
498
  * Start a deferred execution
462
499
  * @experimental
463
500
  */
464
501
  async start(execution) {
465
- return this.dispatcher.start(execution);
502
+ if (typeof execution !== "string" && typeof execution !== "number") {
503
+ if (execution.status !== import_constants.EXECUTION_STATUS.QUEUEING && execution.status !== import_constants.EXECUTION_STATUS.STARTED && execution.status != null) {
504
+ return;
505
+ }
506
+ if (execution.status === import_constants.EXECUTION_STATUS.STARTED && execution.startedAt) {
507
+ return;
508
+ }
509
+ }
510
+ return this.dispatcher.enqueue({ executionId: this.getModelId(execution) });
466
511
  }
467
512
  createProcessor(execution, options = {}) {
468
513
  return new import_Processor.default(execution, { ...options, plugin: this });
469
514
  }
515
+ getModelId(input) {
516
+ return typeof input === "string" || typeof input === "number" ? input : input.id;
517
+ }
470
518
  async execute(workflow, values, options = {}) {
471
519
  const trigger = this.triggers.get(workflow.type);
472
520
  if (!trigger) {
@@ -493,12 +541,304 @@ class PluginWorkflowServer extends import_server.Plugin {
493
541
  return db.sequelize.transaction();
494
542
  }
495
543
  }
544
+ registerTaskStatsProvider(type, provider) {
545
+ this.taskStatsProviders.set(type, provider);
546
+ }
547
+ async getWorkflowIdsByKey(workflowKey, transaction) {
548
+ const WorkflowRepo = this.db.getRepository("workflows");
549
+ const workflows = await WorkflowRepo.find({
550
+ filter: {
551
+ key: workflowKey
552
+ },
553
+ fields: ["id"],
554
+ transaction
555
+ });
556
+ return workflows.map((workflow) => workflow.id);
557
+ }
558
+ buildTaskStatsFilter(options) {
559
+ var _a, _b, _c;
560
+ const filter = {};
561
+ if ((_a = options.userIds) == null ? void 0 : _a.length) {
562
+ filter.userId = options.userIds;
563
+ }
564
+ if ((_b = options.workflowKeys) == null ? void 0 : _b.length) {
565
+ filter.workflowKey = options.workflowKeys;
566
+ }
567
+ if ((_c = options.types) == null ? void 0 : _c.length) {
568
+ filter.type = options.types;
569
+ }
570
+ return filter;
571
+ }
572
+ normalizeTaskStats(stats = {}) {
573
+ return {
574
+ pending: Number(stats.pending) || 0,
575
+ all: Number(stats.all) || 0
576
+ };
577
+ }
578
+ async upsertTaskStatsRow(row, transaction) {
579
+ const repository = this.db.getRepository("userWorkflowTaskStats");
580
+ const values = {
581
+ userId: row.userId,
582
+ workflowKey: row.workflowKey,
583
+ type: row.type,
584
+ ...this.normalizeTaskStats(row)
585
+ };
586
+ const record = await repository.findOne({
587
+ filter: {
588
+ userId: row.userId,
589
+ workflowKey: row.workflowKey,
590
+ type: row.type
591
+ },
592
+ transaction
593
+ });
594
+ if (record) {
595
+ await record.update(values, { transaction });
596
+ return record;
597
+ }
598
+ return repository.create({
599
+ values,
600
+ transaction
601
+ });
602
+ }
603
+ async refreshUserWorkflowTaskTypeStats(userId, type, { transaction } = {}) {
604
+ const repository = this.db.getRepository("userWorkflowTaskStats");
605
+ const rows = await repository.find({
606
+ filter: {
607
+ userId,
608
+ type
609
+ },
610
+ fields: ["pending", "all"],
611
+ transaction
612
+ });
613
+ const stats = rows.reduce(
614
+ (result, row) => ({
615
+ pending: result.pending + (Number(row.pending) || 0),
616
+ all: result.all + (Number(row.all) || 0)
617
+ }),
618
+ { pending: 0, all: 0 }
619
+ );
620
+ await this.updateTasksStats(userId, type, stats, { transaction });
621
+ return stats;
622
+ }
623
+ async refreshUserWorkflowTaskWorkflowStats(userId, workflowKey, { transaction } = {}) {
624
+ const repository = this.db.getRepository("userWorkflowTaskStats");
625
+ const rows = await repository.find({
626
+ filter: {
627
+ userId,
628
+ workflowKey
629
+ },
630
+ fields: ["pending", "all"],
631
+ transaction
632
+ });
633
+ return rows.reduce(
634
+ (result, row) => ({
635
+ pending: result.pending + (Number(row.pending) || 0),
636
+ all: result.all + (Number(row.all) || 0)
637
+ }),
638
+ { pending: 0, all: 0 }
639
+ );
640
+ }
641
+ sendTaskWorkflowStatsUpdated(userId, workflowKey, stats) {
642
+ if (!userId) {
643
+ return;
644
+ }
645
+ this.app.emit("ws:sendToUser", {
646
+ userId,
647
+ message: {
648
+ type: "workflow:taskWorkflowStats:updated",
649
+ payload: {
650
+ userId,
651
+ workflowKey,
652
+ stats
653
+ }
654
+ }
655
+ });
656
+ }
657
+ async updateTaskStatsByWorkflow(input, { transaction } = {}) {
658
+ await this.upsertTaskStatsRow(
659
+ {
660
+ userId: input.userId,
661
+ workflowKey: input.workflowKey,
662
+ type: input.type,
663
+ ...this.normalizeTaskStats(input.stats)
664
+ },
665
+ transaction
666
+ );
667
+ await this.refreshUserWorkflowTaskTypeStats(input.userId, input.type, { transaction });
668
+ const workflowStats = await this.refreshUserWorkflowTaskWorkflowStats(input.userId, input.workflowKey, {
669
+ transaction
670
+ });
671
+ this.sendTaskWorkflowStatsUpdated(input.userId, input.workflowKey, workflowStats);
672
+ }
673
+ async repairTaskStats(options = {}) {
674
+ const run = async (transaction) => {
675
+ var _a, _b;
676
+ const repository = this.db.getRepository("userWorkflowTaskStats");
677
+ const TaskStatsModel = this.db.getModel("userWorkflowTaskStats");
678
+ const filter = this.buildTaskStatsFilter(options);
679
+ const existedRows = await repository.find({
680
+ filter,
681
+ fields: ["userId", "workflowKey", "type"],
682
+ transaction
683
+ });
684
+ const providers = Array.from(this.taskStatsProviders.entries()).filter(([type]) => {
685
+ var _a2;
686
+ return !((_a2 = options.types) == null ? void 0 : _a2.length) || options.types.includes(type);
687
+ });
688
+ const legacyRows = providers.length ? await this.db.getRepository("userWorkflowTasks").find({
689
+ filter: {
690
+ ...((_a = options.userIds) == null ? void 0 : _a.length) ? { userId: options.userIds } : {},
691
+ type: providers.map(([type]) => type)
692
+ },
693
+ fields: ["userId", "type"],
694
+ transaction
695
+ }) : [];
696
+ const collectedRows = (await Promise.all(
697
+ providers.map(([, provider]) => provider.collectTaskStats({ ...options, transaction }, { plugin: this }))
698
+ )).flat();
699
+ const rows = collectedRows.filter((row) => {
700
+ var _a2, _b2, _c;
701
+ if (((_a2 = options.userIds) == null ? void 0 : _a2.length) && !options.userIds.includes(row.userId)) {
702
+ return false;
703
+ }
704
+ if (((_b2 = options.workflowKeys) == null ? void 0 : _b2.length) && !options.workflowKeys.includes(row.workflowKey)) {
705
+ return false;
706
+ }
707
+ if (((_c = options.types) == null ? void 0 : _c.length) && !options.types.includes(row.type)) {
708
+ return false;
709
+ }
710
+ return true;
711
+ });
712
+ await repository.destroy({
713
+ filter,
714
+ transaction
715
+ });
716
+ const batchSize = 1e3;
717
+ for (let offset = 0; offset < rows.length; offset += batchSize) {
718
+ await TaskStatsModel.bulkCreate(rows.slice(offset, offset + batchSize), {
719
+ returning: false,
720
+ transaction
721
+ });
722
+ }
723
+ const typePairs = /* @__PURE__ */ new Set();
724
+ const workflowPairs = /* @__PURE__ */ new Set();
725
+ if ((_b = options.userIds) == null ? void 0 : _b.length) {
726
+ for (const userId of options.userIds) {
727
+ for (const [type] of providers) {
728
+ typePairs.add(`${userId}\0${type}`);
729
+ }
730
+ for (const workflowKey of options.workflowKeys ?? []) {
731
+ workflowPairs.add(`${userId}\0${workflowKey}`);
732
+ }
733
+ }
734
+ }
735
+ for (const row of legacyRows) {
736
+ typePairs.add(`${row.userId}\0${row.type}`);
737
+ }
738
+ for (const row of [...existedRows, ...rows]) {
739
+ typePairs.add(`${row.userId}\0${row.type}`);
740
+ workflowPairs.add(`${row.userId}\0${row.workflowKey}`);
741
+ }
742
+ const affectedUserIds = Array.from(typePairs, (pair) => Number(pair.slice(0, pair.indexOf("\0"))));
743
+ const affectedTypes = Array.from(typePairs, (pair) => pair.slice(pair.indexOf("\0") + 1));
744
+ const categorizedRows = typePairs.size ? await TaskStatsModel.findAll({
745
+ attributes: ["userId", "type", [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("pending")), "pending"], [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("all")), "all"]],
746
+ where: {
747
+ userId: Array.from(new Set(affectedUserIds)),
748
+ type: Array.from(new Set(affectedTypes))
749
+ },
750
+ group: ["userId", "type"],
751
+ raw: true,
752
+ transaction
753
+ }) : [];
754
+ const categorizedMap = new Map(
755
+ categorizedRows.map((row) => [
756
+ `${row.userId}\0${row.type}`,
757
+ this.normalizeTaskStats({ pending: row.pending, all: row.all })
758
+ ])
759
+ );
760
+ const categorizedValues = Array.from(typePairs, (pair) => {
761
+ const separatorIndex = pair.indexOf("\0");
762
+ return {
763
+ userId: Number(pair.slice(0, separatorIndex)),
764
+ type: pair.slice(separatorIndex + 1),
765
+ stats: categorizedMap.get(pair) ?? { pending: 0, all: 0 }
766
+ };
767
+ });
768
+ const categorizedUserIds = Array.from(new Set(affectedUserIds));
769
+ const categorizedTypes = Array.from(new Set(affectedTypes));
770
+ if (categorizedUserIds.length && categorizedTypes.length) {
771
+ await this.db.getRepository("userWorkflowTasks").destroy({
772
+ filter: {
773
+ userId: categorizedUserIds,
774
+ type: categorizedTypes
775
+ },
776
+ transaction
777
+ });
778
+ }
779
+ const UserTasksModel = this.db.getModel("userWorkflowTasks");
780
+ for (let offset = 0; offset < categorizedValues.length; offset += batchSize) {
781
+ await UserTasksModel.bulkCreate(categorizedValues.slice(offset, offset + batchSize), {
782
+ returning: false,
783
+ transaction
784
+ });
785
+ }
786
+ if (options.silent) {
787
+ return;
788
+ }
789
+ for (const value of categorizedValues) {
790
+ if (!value.userId) {
791
+ continue;
792
+ }
793
+ this.app.emit("ws:sendToUser", {
794
+ userId: value.userId,
795
+ message: { type: "workflow:tasks:updated", payload: value }
796
+ });
797
+ }
798
+ const affectedWorkflowUserIds = Array.from(workflowPairs, (pair) => Number(pair.slice(0, pair.indexOf("\0"))));
799
+ const affectedWorkflowKeys = Array.from(workflowPairs, (pair) => pair.slice(pair.indexOf("\0") + 1));
800
+ const workflowRows = workflowPairs.size ? await TaskStatsModel.findAll({
801
+ attributes: [
802
+ "userId",
803
+ "workflowKey",
804
+ [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("pending")), "pending"],
805
+ [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("all")), "all"]
806
+ ],
807
+ where: {
808
+ userId: Array.from(new Set(affectedWorkflowUserIds)),
809
+ workflowKey: Array.from(new Set(affectedWorkflowKeys))
810
+ },
811
+ group: ["userId", "workflowKey"],
812
+ raw: true,
813
+ transaction
814
+ }) : [];
815
+ const workflowMap = new Map(
816
+ workflowRows.map((row) => [
817
+ `${row.userId}\0${row.workflowKey}`,
818
+ this.normalizeTaskStats({ pending: row.pending, all: row.all })
819
+ ])
820
+ );
821
+ for (const pair of workflowPairs) {
822
+ const separatorIndex = pair.indexOf("\0");
823
+ const userId = Number(pair.slice(0, separatorIndex));
824
+ const workflowKey = pair.slice(separatorIndex + 1);
825
+ this.sendTaskWorkflowStatsUpdated(userId, workflowKey, workflowMap.get(pair) ?? { pending: 0, all: 0 });
826
+ }
827
+ };
828
+ if (options.transaction) {
829
+ await run(options.transaction);
830
+ return;
831
+ }
832
+ await this.db.sequelize.transaction(run);
833
+ }
496
834
  /**
497
835
  * @experimental
836
+ * @deprecated Use updateTaskStatsByWorkflow() when workflowKey is available.
498
837
  */
499
838
  async updateTasksStats(userId, type, stats = { pending: 0, all: 0 }, { transaction }) {
500
839
  const { db } = this.app;
501
840
  const repository = db.getRepository("userWorkflowTasks");
841
+ const normalizedStats = this.normalizeTaskStats(stats);
502
842
  let record = await repository.findOne({
503
843
  filter: {
504
844
  userId,
@@ -509,7 +849,7 @@ class PluginWorkflowServer extends import_server.Plugin {
509
849
  if (record) {
510
850
  await record.update(
511
851
  {
512
- stats
852
+ stats: normalizedStats
513
853
  },
514
854
  { transaction }
515
855
  );
@@ -518,7 +858,7 @@ class PluginWorkflowServer extends import_server.Plugin {
518
858
  values: {
519
859
  userId,
520
860
  type,
521
- stats
861
+ stats: normalizedStats
522
862
  },
523
863
  transaction
524
864
  });
@@ -73,6 +73,7 @@ export default class Processor {
73
73
  abortController: AbortController;
74
74
  timeoutGuard: NodeJS.Timeout | null;
75
75
  private runningRegistered;
76
+ private unregisterRunningExecution?;
76
77
  private abortReason;
77
78
  private aborted;
78
79
  constructor(execution: ExecutionModel, options: ProcessorOptions);
@@ -101,13 +102,7 @@ export default class Processor {
101
102
  prepare(): Promise<void>;
102
103
  start(): Promise<any>;
103
104
  resume(job: JobModel): Promise<void>;
104
- resolveRerun(options?: ProcessorRerunOptions): {
105
- node: FlowNodeModel;
106
- input: JobModel | {
107
- result: any;
108
- };
109
- targetJob: JobModel;
110
- };
105
+ private resolveRerun;
111
106
  rerun(options?: ProcessorRerunOptions): Promise<any>;
112
107
  private getRerunNode;
113
108
  private getRerunInput;
@@ -46,6 +46,7 @@ var import_utils = require("@nocobase/utils");
46
46
  var import_set = __toESM(require("lodash/set"));
47
47
  var import_constants = require("./constants");
48
48
  var import_timeout_errors = require("./timeout-errors");
49
+ const JOB_SAVE_BATCH_SIZE = 100;
49
50
  const OPEN_SCOPE_PENDING_ERROR = "Pending jobs are not allowed inside an open transaction scope";
50
51
  class Processor {
51
52
  constructor(execution, options) {
@@ -89,6 +90,7 @@ class Processor {
89
90
  abortController = new AbortController();
90
91
  timeoutGuard = null;
91
92
  runningRegistered = false;
93
+ unregisterRunningExecution;
92
94
  abortReason = null;
93
95
  aborted = false;
94
96
  get abortSignal() {
@@ -460,42 +462,51 @@ class Processor {
460
462
  }
461
463
  if (this.jobsToSave.size) {
462
464
  const newJobs = [];
465
+ const JobCollection = this.options.plugin.db.getCollection("jobs");
466
+ const JobsModel = this.options.plugin.db.getModel("jobs");
463
467
  for (const job of this.jobsToSave.values()) {
464
468
  if (job.isNewRecord) {
465
469
  newJobs.push(job);
466
470
  } else {
467
- const JobCollection = this.options.plugin.db.getCollection("jobs");
468
471
  const changes = [];
469
472
  if (job.changed("status")) {
470
- changes.push([`status`, job.status]);
471
- job.changed("status", false);
473
+ changes.push(["status", job.status]);
472
474
  }
473
475
  if (job.changed("meta")) {
474
- changes.push([`meta`, JSON.stringify(job.meta ?? null)]);
475
- job.changed("meta", false);
476
+ changes.push(["meta", JSON.stringify(job.meta ?? null)]);
476
477
  }
477
478
  if (job.changed("result")) {
478
- changes.push([`result`, JSON.stringify(job.result ?? null)]);
479
- job.changed("result", false);
479
+ changes.push(["result", JSON.stringify(job.result ?? null)]);
480
+ }
481
+ if (job.changed("startedAt")) {
482
+ changes.push(["startedAt", job.startedAt]);
480
483
  }
481
484
  if (changes.length) {
485
+ const idColumn = JobsModel.rawAttributes.id.field || "id";
482
486
  await this.options.plugin.db.sequelize.query(
483
- `UPDATE ${JobCollection.quotedTableName()} SET ${changes.map(([key]) => `${key} = ?`)} WHERE id='${job.id}'`,
487
+ `UPDATE ${JobCollection.quotedTableName()} SET ${changes.map(
488
+ ([field]) => `${this.options.plugin.db.quoteIdentifier(JobsModel.rawAttributes[field].field || field)} = ?`
489
+ )} WHERE ${this.options.plugin.db.quoteIdentifier(idColumn)}='${job.id}'`,
484
490
  { replacements: changes.map(([, value]) => value) }
485
491
  );
492
+ for (const [field] of changes) {
493
+ job.changed(field, false);
494
+ }
486
495
  }
487
496
  }
488
497
  }
489
498
  if (newJobs.length) {
490
- const JobsModel = this.options.plugin.db.getModel("jobs");
491
- await JobsModel.bulkCreate(
492
- newJobs.map((job) => job.toJSON()),
493
- {
494
- returning: false
499
+ for (let offset = 0; offset < newJobs.length; offset += JOB_SAVE_BATCH_SIZE) {
500
+ const batch = newJobs.slice(offset, offset + JOB_SAVE_BATCH_SIZE);
501
+ await JobsModel.bulkCreate(
502
+ batch.map((job) => job.toJSON()),
503
+ {
504
+ returning: false
505
+ }
506
+ );
507
+ for (const job of batch) {
508
+ job.isNewRecord = false;
495
509
  }
496
- );
497
- for (const job of newJobs) {
498
- job.isNewRecord = false;
499
510
  }
500
511
  }
501
512
  this.jobsToSave.clear();
@@ -640,6 +651,7 @@ class Processor {
640
651
  const { database } = this.execution.constructor;
641
652
  const model = database.getModel("jobs");
642
653
  let job;
654
+ const startedAt = Object.prototype.hasOwnProperty.call(payload, "startedAt") ? payload.startedAt : /* @__PURE__ */ new Date();
643
655
  if (payload instanceof model) {
644
656
  job = payload;
645
657
  job.set("updatedAt", /* @__PURE__ */ new Date());
@@ -649,6 +661,7 @@ class Processor {
649
661
  status: payload.status,
650
662
  result: Object.prototype.hasOwnProperty.call(payload, "result") ? payload.result : null,
651
663
  meta: Object.prototype.hasOwnProperty.call(payload, "meta") ? payload.meta : null,
664
+ startedAt,
652
665
  updatedAt: /* @__PURE__ */ new Date()
653
666
  });
654
667
  } else {
@@ -656,6 +669,7 @@ class Processor {
656
669
  {
657
670
  ...payload,
658
671
  id: this.options.plugin.snowflake.getUniqueID().toString(),
672
+ startedAt,
659
673
  createdAt: /* @__PURE__ */ new Date(),
660
674
  updatedAt: /* @__PURE__ */ new Date(),
661
675
  executionId: this.execution.id
@@ -684,7 +698,10 @@ class Processor {
684
698
  this.options.plugin.timeoutManager.clear(this.execution.id);
685
699
  this.abortReason = null;
686
700
  this.aborted = false;
687
- this.options.plugin.registerRunningExecution(this.execution.id, (reason) => this.abortExecution(reason));
701
+ this.unregisterRunningExecution = this.options.plugin.registerRunningExecution(
702
+ this.execution.id,
703
+ (reason) => this.abortExecution(reason)
704
+ );
688
705
  this.runningRegistered = true;
689
706
  const remaining = this.execution.expiresAt ? this.execution.expiresAt.getTime() - Date.now() : null;
690
707
  if (remaining == null) {
@@ -700,6 +717,7 @@ class Processor {
700
717
  return this.options.plugin.timeoutManager.shouldContinue(this.execution);
701
718
  }
702
719
  leaveRunningState() {
720
+ var _a;
703
721
  if (this.timeoutGuard) {
704
722
  clearTimeout(this.timeoutGuard);
705
723
  this.timeoutGuard = null;
@@ -707,7 +725,8 @@ class Processor {
707
725
  if (!this.runningRegistered) {
708
726
  return;
709
727
  }
710
- this.options.plugin.unregisterRunningExecution(this.execution.id);
728
+ (_a = this.unregisterRunningExecution) == null ? void 0 : _a.call(this);
729
+ this.unregisterRunningExecution = void 0;
711
730
  this.runningRegistered = false;
712
731
  }
713
732
  /**
@@ -11,8 +11,8 @@ type AbortHandler = {
11
11
  };
12
12
  export default class RunningExecutionRegistry {
13
13
  private readonly executions;
14
- register(executionId: number | string, handler: AbortHandler): void;
15
- unregister(executionId: number | string): void;
14
+ register(executionId: number | string, handler: AbortHandler): () => void;
15
+ unregister(executionId: number | string, handler?: AbortHandler): void;
16
16
  abort(executionId: number | string, reason?: string): boolean;
17
17
  }
18
18
  export {};
@@ -32,17 +32,32 @@ module.exports = __toCommonJS(RunningExecutionRegistry_exports);
32
32
  class RunningExecutionRegistry {
33
33
  executions = /* @__PURE__ */ new Map();
34
34
  register(executionId, handler) {
35
- this.executions.set(String(executionId), handler);
35
+ const key = String(executionId);
36
+ const handlers = this.executions.get(key) ?? /* @__PURE__ */ new Set();
37
+ handlers.add(handler);
38
+ this.executions.set(key, handlers);
39
+ return () => this.unregister(executionId, handler);
36
40
  }
37
- unregister(executionId) {
38
- this.executions.delete(String(executionId));
41
+ unregister(executionId, handler) {
42
+ const key = String(executionId);
43
+ if (!handler) {
44
+ this.executions.delete(key);
45
+ return;
46
+ }
47
+ const handlers = this.executions.get(key);
48
+ handlers == null ? void 0 : handlers.delete(handler);
49
+ if (!(handlers == null ? void 0 : handlers.size)) {
50
+ this.executions.delete(key);
51
+ }
39
52
  }
40
53
  abort(executionId, reason) {
41
- const handler = this.executions.get(String(executionId));
42
- if (!handler) {
54
+ const handlers = this.executions.get(String(executionId));
55
+ if (!(handlers == null ? void 0 : handlers.size)) {
43
56
  return false;
44
57
  }
45
- handler.abort(reason);
58
+ for (const handler of handlers) {
59
+ handler.abort(reason);
60
+ }
46
61
  return true;
47
62
  }
48
63
  }