@nocobase/plugin-workflow 2.0.57 → 2.0.58

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.
@@ -11,8 +11,8 @@ module.exports = {
11
11
  "react": "18.2.0",
12
12
  "@formily/core": "2.3.7",
13
13
  "@formily/react": "2.3.7",
14
- "@nocobase/client": "2.0.57",
15
- "@nocobase/utils": "2.0.57",
14
+ "@nocobase/client": "2.0.58",
15
+ "@nocobase/utils": "2.0.58",
16
16
  "antd": "5.24.2",
17
17
  "@ant-design/icons": "5.6.1",
18
18
  "react-router-dom": "6.30.1",
@@ -20,20 +20,20 @@ module.exports = {
20
20
  "lodash": "4.18.1",
21
21
  "@dnd-kit/core": "6.1.0",
22
22
  "@formily/shared": "2.3.7",
23
- "@nocobase/flow-engine": "2.0.57",
24
- "@nocobase/plugin-mobile": "2.0.57",
23
+ "@nocobase/flow-engine": "2.0.58",
24
+ "@nocobase/plugin-mobile": "2.0.58",
25
25
  "sequelize": "6.35.2",
26
- "@nocobase/server": "2.0.57",
27
- "@nocobase/database": "2.0.57",
28
- "@nocobase/data-source-manager": "2.0.57",
29
- "@nocobase/logger": "2.0.57",
30
- "@nocobase/evaluators": "2.0.57",
26
+ "@nocobase/server": "2.0.58",
27
+ "@nocobase/database": "2.0.58",
28
+ "@nocobase/data-source-manager": "2.0.58",
29
+ "@nocobase/logger": "2.0.58",
30
+ "@nocobase/evaluators": "2.0.58",
31
31
  "@formily/antd-v5": "1.2.3",
32
32
  "@formily/reactive": "2.3.7",
33
33
  "@emotion/css": "11.13.0",
34
34
  "@formily/json-schema": "2.3.7",
35
- "@nocobase/actions": "2.0.57",
35
+ "@nocobase/actions": "2.0.58",
36
36
  "dayjs": "1.11.13",
37
- "@nocobase/plugin-workflow-test": "2.0.57",
38
- "@nocobase/test": "2.0.57"
37
+ "@nocobase/plugin-workflow-test": "2.0.58",
38
+ "@nocobase/test": "2.0.58"
39
39
  };
@@ -1 +1 @@
1
- {"name":"cron-parser","version":"4.4.0","description":"Node.js library for parsing crontab instructions","main":"lib/parser.js","types":"index.d.ts","typesVersions":{"<4.1":{"*":["types/ts3/*"]}},"directories":{"test":"test"},"scripts":{"test:tsd":"tsd","test:unit":"TZ=UTC tap ./test/*.js","test:cover":"TZ=UTC tap --coverage-report=html ./test/*.js","lint":"eslint .","lint:fix":"eslint --fix .","test":"npm run lint && npm run test:unit && npm run test:tsd"},"repository":{"type":"git","url":"https://github.com/harrisiirak/cron-parser.git"},"keywords":["cron","crontab","parser"],"author":"Harri Siirak","contributors":["Nicholas Clawson","Daniel Prentis <daniel@salsitasoft.com>","Renault John Lecoultre","Richard Astbury <richard.astbury@gmail.com>","Meaglin Wasabi <Meaglin.wasabi@gmail.com>","Mike Kusold <hello@mikekusold.com>","Alex Kit <alex.kit@atmajs.com>","Santiago Gimeno <santiago.gimeno@gmail.com>","Daniel <darc.tec@gmail.com>","Christian Steininger <christian.steininger.cs@gmail.com>","Mykola Piskovyi <m.piskovyi@gmail.com>","Brian Vaughn <brian.david.vaughn@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Yasuhiroki <yasuhiroki.duck@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Brendan Warkentin <faazshift@gmail.com>","Charlie Fish <fishcharlie.code@gmail.com>","Ian Graves <ian+diskimage@iangrav.es>","Andy Thompson <me@andytson.com>","Regev Brody <regevbr@gmail.com>"],"license":"MIT","dependencies":{"luxon":"^1.28.0"},"devDependencies":{"eslint":"^8.2.0","sinon":"^10.0.0","tap":"^16.0.1","tsd":"^0.19.0"},"engines":{"node":">=0.8"},"browser":{"fs":false},"tap":{"check-coverage":false},"tsd":{"directory":"test","compilerOptions":{"lib":["es2017","dom"]}},"_lastModified":"2026-05-25T22:01:52.509Z"}
1
+ {"name":"cron-parser","version":"4.4.0","description":"Node.js library for parsing crontab instructions","main":"lib/parser.js","types":"index.d.ts","typesVersions":{"<4.1":{"*":["types/ts3/*"]}},"directories":{"test":"test"},"scripts":{"test:tsd":"tsd","test:unit":"TZ=UTC tap ./test/*.js","test:cover":"TZ=UTC tap --coverage-report=html ./test/*.js","lint":"eslint .","lint:fix":"eslint --fix .","test":"npm run lint && npm run test:unit && npm run test:tsd"},"repository":{"type":"git","url":"https://github.com/harrisiirak/cron-parser.git"},"keywords":["cron","crontab","parser"],"author":"Harri Siirak","contributors":["Nicholas Clawson","Daniel Prentis <daniel@salsitasoft.com>","Renault John Lecoultre","Richard Astbury <richard.astbury@gmail.com>","Meaglin Wasabi <Meaglin.wasabi@gmail.com>","Mike Kusold <hello@mikekusold.com>","Alex Kit <alex.kit@atmajs.com>","Santiago Gimeno <santiago.gimeno@gmail.com>","Daniel <darc.tec@gmail.com>","Christian Steininger <christian.steininger.cs@gmail.com>","Mykola Piskovyi <m.piskovyi@gmail.com>","Brian Vaughn <brian.david.vaughn@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Yasuhiroki <yasuhiroki.duck@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Brendan Warkentin <faazshift@gmail.com>","Charlie Fish <fishcharlie.code@gmail.com>","Ian Graves <ian+diskimage@iangrav.es>","Andy Thompson <me@andytson.com>","Regev Brody <regevbr@gmail.com>"],"license":"MIT","dependencies":{"luxon":"^1.28.0"},"devDependencies":{"eslint":"^8.2.0","sinon":"^10.0.0","tap":"^16.0.1","tsd":"^0.19.0"},"engines":{"node":">=0.8"},"browser":{"fs":false},"tap":{"check-coverage":false},"tsd":{"directory":"test","compilerOptions":{"lib":["es2017","dom"]}},"_lastModified":"2026-05-28T02:36:20.282Z"}
@@ -1 +1 @@
1
- {"name":"lru-cache","description":"A cache object that deletes the least-recently-used items.","version":"8.0.5","author":"Isaac Z. Schlueter <i@izs.me>","keywords":["mru","lru","cache"],"sideEffects":false,"scripts":{"build":"npm run prepare","preprepare":"rm -rf dist","prepare":"tsc -p tsconfig.json && tsc -p tsconfig-esm.json","postprepare":"bash fixup.sh","pretest":"npm run prepare","presnap":"npm run prepare","test":"c8 tap","snap":"c8 tap","preversion":"npm test","postversion":"npm publish","prepublishOnly":"git push origin --follow-tags","format":"prettier --write .","typedoc":"typedoc --tsconfig tsconfig-esm.json ./src/*.ts","benchmark-results-typedoc":"bash scripts/benchmark-results-typedoc.sh","prebenchmark":"npm run prepare","benchmark":"make -C benchmark","preprofile":"npm run prepare","profile":"make -C benchmark profile"},"main":"./dist/cjs/index-cjs.js","module":"./dist/mjs/index.js","types":"./dist/mjs/index.d.ts","exports":{"./min":{"import":{"types":"./dist/mjs/index.d.ts","default":"./dist/mjs/index.min.js"},"require":{"types":"./dist/cjs/index.d.ts","default":"./dist/cjs/index.min.js"}},".":{"import":{"types":"./dist/mjs/index.d.ts","default":"./dist/mjs/index.js"},"require":{"types":"./dist/cjs/index.d.ts","default":"./dist/cjs/index-cjs.js"}}},"repository":"git://github.com/isaacs/node-lru-cache.git","devDependencies":{"@size-limit/preset-small-lib":"^7.0.8","@types/node":"^17.0.31","@types/tap":"^15.0.6","benchmark":"^2.1.4","c8":"^7.11.2","clock-mock":"^1.0.6","esbuild":"^0.17.11","eslint-config-prettier":"^8.5.0","marked":"^4.2.12","mkdirp":"^2.1.5","prettier":"^2.6.2","size-limit":"^7.0.8","tap":"^16.3.4","ts-node":"^10.7.0","tslib":"^2.4.0","typedoc":"^0.23.24","typescript":"^4.6.4"},"license":"ISC","files":["dist"],"engines":{"node":">=16.14"},"prettier":{"semi":false,"printWidth":70,"tabWidth":2,"useTabs":false,"singleQuote":true,"jsxSingleQuote":false,"bracketSameLine":true,"arrowParens":"avoid","endOfLine":"lf"},"tap":{"coverage":false,"node-arg":["--expose-gc","--no-warnings","--loader","ts-node/esm"],"ts":false},"size-limit":[{"path":"./dist/mjs/index.js"}],"_lastModified":"2026-05-25T22:01:52.167Z"}
1
+ {"name":"lru-cache","description":"A cache object that deletes the least-recently-used items.","version":"8.0.5","author":"Isaac Z. Schlueter <i@izs.me>","keywords":["mru","lru","cache"],"sideEffects":false,"scripts":{"build":"npm run prepare","preprepare":"rm -rf dist","prepare":"tsc -p tsconfig.json && tsc -p tsconfig-esm.json","postprepare":"bash fixup.sh","pretest":"npm run prepare","presnap":"npm run prepare","test":"c8 tap","snap":"c8 tap","preversion":"npm test","postversion":"npm publish","prepublishOnly":"git push origin --follow-tags","format":"prettier --write .","typedoc":"typedoc --tsconfig tsconfig-esm.json ./src/*.ts","benchmark-results-typedoc":"bash scripts/benchmark-results-typedoc.sh","prebenchmark":"npm run prepare","benchmark":"make -C benchmark","preprofile":"npm run prepare","profile":"make -C benchmark profile"},"main":"./dist/cjs/index-cjs.js","module":"./dist/mjs/index.js","types":"./dist/mjs/index.d.ts","exports":{"./min":{"import":{"types":"./dist/mjs/index.d.ts","default":"./dist/mjs/index.min.js"},"require":{"types":"./dist/cjs/index.d.ts","default":"./dist/cjs/index.min.js"}},".":{"import":{"types":"./dist/mjs/index.d.ts","default":"./dist/mjs/index.js"},"require":{"types":"./dist/cjs/index.d.ts","default":"./dist/cjs/index-cjs.js"}}},"repository":"git://github.com/isaacs/node-lru-cache.git","devDependencies":{"@size-limit/preset-small-lib":"^7.0.8","@types/node":"^17.0.31","@types/tap":"^15.0.6","benchmark":"^2.1.4","c8":"^7.11.2","clock-mock":"^1.0.6","esbuild":"^0.17.11","eslint-config-prettier":"^8.5.0","marked":"^4.2.12","mkdirp":"^2.1.5","prettier":"^2.6.2","size-limit":"^7.0.8","tap":"^16.3.4","ts-node":"^10.7.0","tslib":"^2.4.0","typedoc":"^0.23.24","typescript":"^4.6.4"},"license":"ISC","files":["dist"],"engines":{"node":">=16.14"},"prettier":{"semi":false,"printWidth":70,"tabWidth":2,"useTabs":false,"singleQuote":true,"jsxSingleQuote":false,"bracketSameLine":true,"arrowParens":"avoid","endOfLine":"lf"},"tap":{"coverage":false,"node-arg":["--expose-gc","--no-warnings","--loader","ts-node/esm"],"ts":false},"size-limit":[{"path":"./dist/mjs/index.js"}],"_lastModified":"2026-05-28T02:36:19.962Z"}
@@ -1 +1 @@
1
- {"name":"nodejs-snowflake","collaborators":["Utkarsh Srivastava <utkarsh@sagacious.dev>"],"description":"Generate time sortable 64 bits unique ids for distributed systems (inspired from twitter snowflake)","version":"2.0.1","license":"Apache 2.0","repository":{"type":"git","url":"https://github.com/utkarsh-pro/nodejs-snowflake.git"},"files":["nodejs_snowflake_bg.wasm","nodejs_snowflake.js","nodejs_snowflake.d.ts"],"main":"nodejs_snowflake.js","types":"nodejs_snowflake.d.ts","_lastModified":"2026-05-25T22:01:51.959Z"}
1
+ {"name":"nodejs-snowflake","collaborators":["Utkarsh Srivastava <utkarsh@sagacious.dev>"],"description":"Generate time sortable 64 bits unique ids for distributed systems (inspired from twitter snowflake)","version":"2.0.1","license":"Apache 2.0","repository":{"type":"git","url":"https://github.com/utkarsh-pro/nodejs-snowflake.git"},"files":["nodejs_snowflake_bg.wasm","nodejs_snowflake.js","nodejs_snowflake.d.ts"],"main":"nodejs_snowflake.js","types":"nodejs_snowflake.d.ts","_lastModified":"2026-05-28T02:36:19.751Z"}
@@ -8,13 +8,17 @@
8
8
  */
9
9
  import { Transactionable } from 'sequelize';
10
10
  import type { QueueEventOptions } from '@nocobase/server';
11
- import Processor from './Processor';
11
+ import Processor, { ProcessorRerunOptions } from './Processor';
12
12
  import type { ExecutionModel, JobModel, WorkflowModel } from './types';
13
13
  import type PluginWorkflowServer from './Plugin';
14
14
  type Pending = {
15
15
  execution: ExecutionModel;
16
16
  job?: JobModel;
17
17
  loaded?: boolean;
18
+ rerun?: ProcessorRerunOptions;
19
+ };
20
+ type RunOptions = {
21
+ dispatch?: boolean;
18
22
  };
19
23
  export type EventOptions = {
20
24
  eventKey?: string;
@@ -41,16 +45,18 @@ export default class Dispatcher {
41
45
  getEventsCount(): number;
42
46
  trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
43
47
  private prepare;
44
- resume(job: any): Promise<void>;
48
+ resume(job: JobModel): Promise<void>;
45
49
  start(execution: ExecutionModel): Promise<void>;
46
50
  beforeStop(): Promise<void>;
47
51
  dispatch(): void;
48
- run(pending: Pending): Promise<void>;
52
+ run(pending: Pending, options?: RunOptions): Promise<void>;
49
53
  private triggerSync;
50
54
  private validateEvent;
51
55
  private createExecution;
52
56
  private acquirePendingExecution;
53
57
  private acquireQueueingExecution;
58
+ private getExecutionLockKey;
59
+ private isLockAcquireError;
54
60
  private process;
55
61
  }
56
62
  export {};
@@ -115,7 +115,7 @@ class Dispatcher {
115
115
  logger.info(`preparing execution for event`);
116
116
  try {
117
117
  const execution = await this.createExecution(...event);
118
- if (!(execution == null ? void 0 : execution.dispatched)) {
118
+ if (!execution.dispatched) {
119
119
  if (this.plugin.serving() && !this.executing && !this.pending.length) {
120
120
  logger.info(`local pending list is empty, adding execution (${execution.id}) to pending list`);
121
121
  this.pending.push({ execution });
@@ -196,11 +196,12 @@ class Dispatcher {
196
196
  this.executing = (async () => {
197
197
  let next = null;
198
198
  let execution = null;
199
+ let pending = null;
199
200
  if (this.pending.length) {
200
- const pending = this.pending.shift();
201
+ pending = this.pending.shift();
201
202
  execution = pending.loaded ? pending.execution : await this.acquirePendingExecution(pending.execution);
202
203
  if (execution) {
203
- next = [execution, pending.job];
204
+ next = [execution, pending.job, pending.rerun];
204
205
  this.plugin.getLogger(next[0].workflowId).info(`pending execution (${next[0].id}) ready to process`);
205
206
  }
206
207
  } else {
@@ -216,7 +217,14 @@ class Dispatcher {
216
217
  }
217
218
  }
218
219
  if (next) {
219
- await this.process(...next);
220
+ try {
221
+ await this.process(next[0], next[1], { rerun: next[2] });
222
+ } catch (error) {
223
+ this.plugin.getLogger(next[0].workflowId).error(`execution (${next[0].id}) process failed`, { error });
224
+ if (pending && this.isLockAcquireError(error)) {
225
+ this.pending.unshift({ ...pending, execution: next[0], loaded: true });
226
+ }
227
+ }
220
228
  }
221
229
  setImmediate(() => {
222
230
  this.executing = null;
@@ -227,9 +235,11 @@ class Dispatcher {
227
235
  });
228
236
  })();
229
237
  }
230
- async run(pending) {
238
+ async run(pending, options = {}) {
231
239
  this.pending.push(pending);
232
- this.dispatch();
240
+ if (options.dispatch !== false) {
241
+ this.dispatch();
242
+ }
233
243
  }
234
244
  async triggerSync(workflow, context, { deferred, ...options } = {}) {
235
245
  let execution;
@@ -252,7 +262,7 @@ class Dispatcher {
252
262
  if (!triggerValid) {
253
263
  return false;
254
264
  }
255
- const { stack } = options;
265
+ const { stack = [] } = options;
256
266
  let valid = true;
257
267
  if ((stack == null ? void 0 : stack.length) > 0) {
258
268
  const existed = await workflow.countExecutions({
@@ -282,7 +292,7 @@ class Dispatcher {
282
292
  await transaction.commit();
283
293
  }
284
294
  (_a = options.onTriggerFail) == null ? void 0 : _a.call(options, workflow, context, options);
285
- return Promise.reject(new Error("event is not valid"));
295
+ throw new Error("event is not valid");
286
296
  }
287
297
  let execution;
288
298
  try {
@@ -391,26 +401,42 @@ class Dispatcher {
391
401
  }
392
402
  return fetched;
393
403
  }
394
- async process(execution, job, options = {}) {
395
- var _a, _b;
404
+ getExecutionLockKey(executionId) {
405
+ return `workflow:execution:${executionId}`;
406
+ }
407
+ isLockAcquireError(error) {
408
+ return error instanceof Error && error.constructor.name === "LockAcquireError";
409
+ }
410
+ async process(execution, job = null, options = {}) {
411
+ const { rerun, ...processorOptions } = options;
396
412
  const logger = this.plugin.getLogger(execution.workflowId);
397
- if (!execution.dispatched) {
398
- const transaction = await this.plugin.useDataSourceTransaction("main", options.transaction);
399
- await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
400
- logger.info(`execution (${execution.id}) from pending list updated to started`);
401
- }
402
- const processor = this.plugin.createProcessor(execution, options);
403
- logger.info(`execution (${execution.id}) ${job ? "resuming" : "starting"}...`);
404
- try {
405
- await (job ? processor.resume(job) : processor.start());
406
- logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
407
- logger.debug(`execution (${execution.id}) details:`, { execution });
408
- if (execution.status && ((_b = (_a = execution.workflow.options) == null ? void 0 : _a.deleteExecutionOnStatus) == null ? void 0 : _b.includes(execution.status))) {
409
- await execution.destroy({ transaction: processor.mainTransaction });
413
+ const run = async () => {
414
+ var _a, _b, _c;
415
+ if (!execution.dispatched) {
416
+ const transaction = await this.plugin.useDataSourceTransaction("main", processorOptions.transaction);
417
+ await execution.update({ dispatched: true, status: import_constants.EXECUTION_STATUS.STARTED }, { transaction });
418
+ logger.info(`execution (${execution.id}) from pending list updated to started`);
410
419
  }
411
- } catch (err) {
412
- logger.error(`execution (${execution.id}) error: ${err.message}`, err);
420
+ const processor = this.plugin.createProcessor(execution, processorOptions);
421
+ logger.info(`execution (${execution.id}) ${rerun ? "rerunning" : job ? "resuming" : "starting"}...`);
422
+ try {
423
+ await (rerun ? processor.rerun(rerun) : job ? processor.resume(job) : processor.start());
424
+ logger.info(`execution (${execution.id}) finished with status: ${execution.status}`);
425
+ logger.debug(`execution (${execution.id}) details:`, { execution });
426
+ if (execution.status && ((_c = (_b = (_a = execution.workflow) == null ? void 0 : _a.options) == null ? void 0 : _b.deleteExecutionOnStatus) == null ? void 0 : _c.includes(execution.status))) {
427
+ await execution.destroy({ transaction: processor.mainTransaction });
428
+ }
429
+ } catch (err) {
430
+ logger.error(`execution (${execution.id}) error: ${err.message}`, err);
431
+ }
432
+ return processor;
433
+ };
434
+ const lock = await this.plugin.app.lockManager.tryAcquire(this.getExecutionLockKey(execution.id), 6e4);
435
+ try {
436
+ return await lock.runExclusive(run, 6e4);
437
+ } catch (error) {
438
+ logger.error(`execution (${execution.id}) could not acquire process lock`, { error });
439
+ throw error;
413
440
  }
414
- return processor;
415
441
  }
416
442
  }
@@ -63,7 +63,8 @@ export default class PluginWorkflowServer extends Plugin {
63
63
  load(): Promise<void>;
64
64
  private toggle;
65
65
  trigger(workflow: WorkflowModel, context: object, options?: EventOptions): void | Promise<Processor | null>;
66
- run(pending: Parameters<Dispatcher['run']>[0]): Promise<void>;
66
+ run(pending: Parameters<Dispatcher['run']>[0], options?: Parameters<Dispatcher['run']>[1]): Promise<void>;
67
+ dispatch(): void;
67
68
  resume(job: any): Promise<void>;
68
69
  /**
69
70
  * Start a deferred execution
@@ -406,8 +406,11 @@ class PluginWorkflowServer extends import_server.Plugin {
406
406
  trigger(workflow, context, options = {}) {
407
407
  return this.dispatcher.trigger(workflow, context, options);
408
408
  }
409
- async run(pending) {
410
- return this.dispatcher.run(pending);
409
+ async run(pending, options) {
410
+ return this.dispatcher.run(pending, options);
411
+ }
412
+ dispatch() {
413
+ return this.dispatcher.dispatch();
411
414
  }
412
415
  async resume(job) {
413
416
  return this.dispatcher.resume(job);
@@ -10,6 +10,13 @@ import { Transaction, Transactionable } from '@nocobase/database';
10
10
  import { Logger } from '@nocobase/logger';
11
11
  import type Plugin from './Plugin';
12
12
  import type { ExecutionModel, FlowNodeModel, JobModel } from './types';
13
+ export type ProcessorRunOptions = {
14
+ rerun?: true;
15
+ };
16
+ export type ProcessorRerunOptions = {
17
+ nodeId?: string | number;
18
+ overwrite?: boolean;
19
+ };
13
20
  export interface ProcessorOptions extends Transactionable {
14
21
  plugin: Plugin;
15
22
  [key: string]: any;
@@ -47,6 +54,7 @@ export default class Processor {
47
54
  private jobsMapByNodeKey;
48
55
  private jobResultsMapByNodeKey;
49
56
  private jobsToSave;
57
+ private rerunContext;
50
58
  /**
51
59
  * @experimental
52
60
  */
@@ -57,8 +65,18 @@ export default class Processor {
57
65
  prepare(): Promise<void>;
58
66
  start(): Promise<void>;
59
67
  resume(job: JobModel): Promise<void>;
68
+ resolveRerun(options?: ProcessorRerunOptions): {
69
+ node: FlowNodeModel;
70
+ input: JobModel | {
71
+ result: any;
72
+ };
73
+ targetJob: JobModel;
74
+ };
75
+ rerun(options?: ProcessorRerunOptions): Promise<any>;
76
+ private getRerunNode;
77
+ private getRerunInput;
60
78
  private exec;
61
- run(node: any, input?: any): any;
79
+ run(node: any, input?: any, options?: ProcessorRunOptions): any;
62
80
  end(node: any, job: JobModel): Promise<any>;
63
81
  private recall;
64
82
  exit(s?: number | true): Promise<any>;
@@ -82,6 +82,7 @@ class Processor {
82
82
  jobsMapByNodeKey = {};
83
83
  jobResultsMapByNodeKey = {};
84
84
  jobsToSave = /* @__PURE__ */ new Map();
85
+ rerunContext = null;
85
86
  /**
86
87
  * @experimental
87
88
  */
@@ -122,6 +123,9 @@ class Processor {
122
123
  if (!execution.workflow) {
123
124
  execution.workflow = plugin.enabledCache.get(execution.workflowId) || await execution.getWorkflow({ transaction });
124
125
  }
126
+ if (!execution.workflow) {
127
+ throw new Error(`workflow (#${execution.workflowId}) not found for execution (#${execution.id})`);
128
+ }
125
129
  const nodes = execution.workflow.nodes || await execution.workflow.getNodes({ transaction });
126
130
  execution.workflow.nodes = nodes;
127
131
  this.makeNodes(nodes);
@@ -173,11 +177,64 @@ class Processor {
173
177
  const node = this.nodesMap.get(job.nodeId);
174
178
  await this.recall(node, job);
175
179
  }
176
- async exec(instruction, node, prevJob) {
180
+ resolveRerun(options = {}) {
181
+ const node = this.getRerunNode(options.nodeId);
182
+ const targetJob = this.jobsMapByNodeKey[node.key];
183
+ if (options.nodeId != null && !targetJob) {
184
+ throw new Error(`job of node (#${node.id}) not found in execution (#${this.execution.id})`);
185
+ }
186
+ if (options.nodeId == null && options.overwrite && !targetJob) {
187
+ throw new Error(`job of head node (#${node.id}) not found in execution (#${this.execution.id})`);
188
+ }
189
+ const input = this.getRerunInput(node);
190
+ return { node, input, targetJob };
191
+ }
192
+ async rerun(options = {}) {
193
+ const { execution } = this;
194
+ if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
195
+ throw new Error(`execution (#${execution.id}) is not started`);
196
+ }
197
+ await this.prepare();
198
+ const { node, input, targetJob } = this.resolveRerun(options);
199
+ this.rerunContext = {
200
+ overwrite: options.overwrite === true,
201
+ targetJob
202
+ };
203
+ try {
204
+ return await this.run(node, input, { rerun: true });
205
+ } finally {
206
+ this.rerunContext = null;
207
+ }
208
+ }
209
+ getRerunNode(nodeId) {
210
+ if (nodeId != null) {
211
+ const node = this.nodesMap.get(nodeId) || this.nodes.find((item) => String(item.id) === String(nodeId));
212
+ if (!node) {
213
+ throw new Error(`node (#${nodeId}) not found in workflow (#${this.execution.workflowId})`);
214
+ }
215
+ return node;
216
+ }
217
+ const head = this.nodes.find((item) => !item.upstream);
218
+ if (!head) {
219
+ throw new Error(`head node not found in workflow (#${this.execution.workflowId})`);
220
+ }
221
+ return head;
222
+ }
223
+ getRerunInput(node) {
224
+ if (!node.upstream) {
225
+ return { result: this.execution.context };
226
+ }
227
+ const upstreamJob = this.jobsMapByNodeKey[node.upstream.key];
228
+ if (!upstreamJob) {
229
+ throw new Error(`upstream job of node (#${node.id}) not found in execution (#${this.execution.id})`);
230
+ }
231
+ return upstreamJob;
232
+ }
233
+ async exec(instruction, node, prevJob, options) {
177
234
  let job;
178
235
  try {
179
236
  this.logger.debug(`config of node`, { data: node.config, workflowId: node.workflowId });
180
- job = await instruction(node, prevJob, this);
237
+ job = await instruction(node, prevJob, this, options);
181
238
  if (job === null) {
182
239
  return this.exit();
183
240
  }
@@ -219,7 +276,7 @@ class Processor {
219
276
  }
220
277
  return this.end(node, savedJob);
221
278
  }
222
- async run(node, input) {
279
+ async run(node, input, options) {
223
280
  const { instructions } = this.options.plugin;
224
281
  const instruction = instructions.get(node.type);
225
282
  if (!instruction) {
@@ -231,7 +288,7 @@ class Processor {
231
288
  this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id})`, {
232
289
  workflowId: node.workflowId
233
290
  });
234
- return this.exec(instruction.run.bind(instruction), node, input);
291
+ return this.exec(instruction.run.bind(instruction), node, input, options);
235
292
  }
236
293
  // parent node should take over the control
237
294
  async end(node, job) {
@@ -325,12 +382,21 @@ class Processor {
325
382
  * @experimental
326
383
  */
327
384
  saveJob(payload) {
385
+ var _a;
328
386
  const { database } = this.execution.constructor;
329
387
  const { model } = database.getCollection("jobs");
330
388
  let job;
331
389
  if (payload instanceof model) {
332
390
  job = payload;
333
391
  job.set("updatedAt", /* @__PURE__ */ new Date());
392
+ } else if (((_a = this.rerunContext) == null ? void 0 : _a.overwrite) && this.rerunContext.targetJob && this.rerunContext.targetJob.nodeId === payload.nodeId) {
393
+ job = this.rerunContext.targetJob;
394
+ job.set({
395
+ status: payload.status,
396
+ result: Object.prototype.hasOwnProperty.call(payload, "result") ? payload.result : null,
397
+ meta: Object.prototype.hasOwnProperty.call(payload, "meta") ? payload.meta : null,
398
+ updatedAt: /* @__PURE__ */ new Date()
399
+ });
334
400
  } else {
335
401
  job = model.build(
336
402
  {
@@ -6,6 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { Context } from '@nocobase/actions';
10
- export declare function destroy(context: Context, next: any): Promise<void>;
11
- export declare function cancel(context: Context, next: any): Promise<never>;
9
+ import { Context, Next } from '@nocobase/actions';
10
+ export declare function destroy(context: Context, next: Next): Promise<void>;
11
+ export declare function cancel(context: Context, next: Next): Promise<never>;
12
+ export declare function rerun(context: Context, next: Next): Promise<never>;
@@ -37,12 +37,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
37
37
  var executions_exports = {};
38
38
  __export(executions_exports, {
39
39
  cancel: () => cancel,
40
- destroy: () => destroy
40
+ destroy: () => destroy,
41
+ rerun: () => rerun
41
42
  });
42
43
  module.exports = __toCommonJS(executions_exports);
43
44
  var import_actions = __toESM(require("@nocobase/actions"));
44
45
  var import_database = require("@nocobase/database");
46
+ var import_Plugin = __toESM(require("../Plugin"));
45
47
  var import_constants = require("../constants");
48
+ function getExecutionLockKey(executionId) {
49
+ return `workflow:execution:${executionId}`;
50
+ }
51
+ function isLockAcquireError(error) {
52
+ return error instanceof Error && error.constructor.name === "LockAcquireError";
53
+ }
46
54
  async function destroy(context, next) {
47
55
  context.action.mergeParams({
48
56
  filter: {
@@ -67,30 +75,90 @@ async function cancel(context, next) {
67
75
  if (execution.status) {
68
76
  return context.throw(400);
69
77
  }
70
- await context.db.sequelize.transaction(async (transaction) => {
71
- await execution.update(
78
+ try {
79
+ const lock = await context.app.lockManager.tryAcquire(getExecutionLockKey(execution.id));
80
+ await lock.runExclusive(async () => {
81
+ await context.db.sequelize.transaction(async (transaction) => {
82
+ await execution.update(
83
+ {
84
+ status: import_constants.EXECUTION_STATUS.ABORTED
85
+ },
86
+ { transaction }
87
+ );
88
+ const pendingJobs = execution.jobs.filter((job) => job.status === import_constants.JOB_STATUS.PENDING);
89
+ await JobRepo.update({
90
+ values: {
91
+ status: import_constants.JOB_STATUS.ABORTED
92
+ },
93
+ filter: {
94
+ id: pendingJobs.map((job) => job.id)
95
+ },
96
+ individualHooks: false,
97
+ transaction
98
+ });
99
+ });
100
+ }, 6e4);
101
+ } catch (error) {
102
+ if (isLockAcquireError(error)) {
103
+ return context.throw(409, "Execution is being processed");
104
+ }
105
+ throw error;
106
+ }
107
+ context.body = execution;
108
+ await next();
109
+ }
110
+ async function rerun(context, next) {
111
+ const workflowPlugin = context.app.pm.get(import_Plugin.default);
112
+ const { filterByTk, values = {} } = context.action.params;
113
+ const { nodeId, overwrite } = values;
114
+ const ExecutionRepo = context.db.getRepository("executions");
115
+ const execution = await ExecutionRepo.findOne({
116
+ filterByTk
117
+ });
118
+ if (!execution) {
119
+ return context.throw(404);
120
+ }
121
+ if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
122
+ return context.throw(409, "Only started executions can be rerun");
123
+ }
124
+ try {
125
+ const lock = await context.app.lockManager.tryAcquire(getExecutionLockKey(execution.id));
126
+ await lock.runExclusive(async () => {
127
+ const processor = workflowPlugin.createProcessor(execution);
128
+ await processor.prepare();
129
+ processor.resolveRerun({
130
+ nodeId,
131
+ overwrite: overwrite === true
132
+ });
133
+ }, 6e4);
134
+ await workflowPlugin.run(
72
135
  {
73
- status: import_constants.EXECUTION_STATUS.ABORTED
136
+ execution,
137
+ loaded: true,
138
+ rerun: {
139
+ nodeId,
140
+ overwrite: overwrite === true
141
+ }
74
142
  },
75
- { transaction }
143
+ { dispatch: false }
76
144
  );
77
- const pendingJobs = execution.jobs.filter((job) => job.status === import_constants.JOB_STATUS.PENDING);
78
- await JobRepo.update({
79
- values: {
80
- status: import_constants.JOB_STATUS.ABORTED
81
- },
82
- filter: {
83
- id: pendingJobs.map((job) => job.id)
84
- },
85
- individualHooks: false,
86
- transaction
87
- });
88
- });
145
+ workflowPlugin.dispatch();
146
+ } catch (error) {
147
+ if (isLockAcquireError(error)) {
148
+ return context.throw(409, "Execution is being processed");
149
+ }
150
+ if (error instanceof Error) {
151
+ return context.throw(400, error.message);
152
+ }
153
+ throw error;
154
+ }
89
155
  context.body = execution;
156
+ context.status = 202;
90
157
  await next();
91
158
  }
92
159
  // Annotate the CommonJS export names for ESM import in node:
93
160
  0 && (module.exports = {
94
161
  cancel,
95
- destroy
162
+ destroy,
163
+ rerun
96
164
  });
@@ -41,6 +41,12 @@ __export(jobs_exports, {
41
41
  module.exports = __toCommonJS(jobs_exports);
42
42
  var import_actions = require("@nocobase/actions");
43
43
  var import_Plugin = __toESM(require("../Plugin"));
44
+ function getExecutionLockKey(executionId) {
45
+ return `workflow:execution:${executionId}`;
46
+ }
47
+ function isLockAcquireError(error) {
48
+ return error instanceof Error && error.constructor.name === "LockAcquireError";
49
+ }
44
50
  async function resume(context, next) {
45
51
  const repository = import_actions.utils.getRepositoryFromParams(context);
46
52
  const workflowPlugin = context.app.pm.get(import_Plugin.default);
@@ -51,8 +57,21 @@ async function resume(context, next) {
51
57
  if (!job) {
52
58
  return context.throw(404, "Job not found");
53
59
  }
60
+ if (!job.execution) {
61
+ job.execution = await job.getExecution();
62
+ }
54
63
  workflowPlugin.getLogger(job.workflowId).warn(`Resuming job #${job.id}...`);
55
- await job.update(values);
64
+ try {
65
+ const lock = await context.app.lockManager.tryAcquire(getExecutionLockKey(job.execution.id));
66
+ await lock.runExclusive(async () => {
67
+ await job.update(values);
68
+ }, 6e4);
69
+ } catch (error) {
70
+ if (isLockAcquireError(error)) {
71
+ return context.throw(409, "Execution is being processed");
72
+ }
73
+ throw error;
74
+ }
56
75
  context.body = job;
57
76
  context.status = 202;
58
77
  await next();
@@ -24,7 +24,9 @@ export interface IJob {
24
24
  * 3. `void` | Promise<void>: processor will do nothing, and terminate the current execution without any action.
25
25
  */
26
26
  export type InstructionResult = IJob | Promise<IJob> | Promise<void> | Promise<null> | null | void;
27
- export type Runner = (node: FlowNodeModel, input: any, processor: Processor) => InstructionResult;
27
+ export type Runner = (node: FlowNodeModel, input: any, processor: Processor, options?: {
28
+ rerun?: true;
29
+ }) => InstructionResult;
28
30
  export type InstructionInterface = {
29
31
  run: Runner;
30
32
  resume?: Runner;
@@ -37,6 +39,8 @@ export type InstructionInterface = {
37
39
  export declare abstract class Instruction implements InstructionInterface {
38
40
  workflow: Plugin;
39
41
  constructor(workflow: Plugin);
40
- abstract run(node: FlowNodeModel, input: any, processor: Processor): InstructionResult;
42
+ abstract run(node: FlowNodeModel, input: any, processor: Processor, options?: {
43
+ rerun?: true;
44
+ }): InstructionResult;
41
45
  }
42
46
  export default Instruction;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.",
7
7
  "description.zh-CN": "一个强大的 BPM 工具,为业务自动化提供基础支持,并且可任意扩展更多的触发器和节点。",
8
8
  "description.ru-RU": "Мощный инструмент BPM, обеспечивающий базовую поддержку автоматизации бизнес-процессов с возможностью неограниченного расширения триггеров и узлов.",
9
- "version": "2.0.57",
9
+ "version": "2.0.58",
10
10
  "license": "Apache-2.0",
11
11
  "main": "./dist/server/index.js",
12
12
  "homepage": "https://docs.nocobase.com/handbook/workflow",
@@ -48,7 +48,7 @@
48
48
  "@nocobase/test": "2.x",
49
49
  "@nocobase/utils": "2.x"
50
50
  },
51
- "gitHead": "629bf05e63bca0cbd60fffb87057d45bda5d9222",
51
+ "gitHead": "158b99ec4f71555fc5125f61cfcf357763994da0",
52
52
  "keywords": [
53
53
  "Workflow"
54
54
  ]