@nocobase/plugin-workflow-javascript 3.0.0-alpha.1 → 3.0.0-alpha.12

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.
@@ -0,0 +1,317 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var TaskConsumer_exports = {};
28
+ __export(TaskConsumer_exports, {
29
+ TaskConsumer: () => TaskConsumer
30
+ });
31
+ module.exports = __toCommonJS(TaskConsumer_exports);
32
+ var import_plugin_workflow = require("@nocobase/plugin-workflow");
33
+ var import_constants = require("../common/constants");
34
+ var import_ScriptWorkerRunner = require("./ScriptWorkerRunner");
35
+ function isTaskMessage(message) {
36
+ return typeof message === "object" && message !== null && "jobId" in message;
37
+ }
38
+ function isScriptArguments(value) {
39
+ return value === null || Array.isArray(value) || typeof value === "object" && value !== null;
40
+ }
41
+ function getJavaScriptArguments(job) {
42
+ if (!job.meta || !("args" in job.meta) || !isScriptArguments(job.meta.args)) {
43
+ return {};
44
+ }
45
+ return job.meta.args;
46
+ }
47
+ function getErrorMessage(error) {
48
+ return error instanceof Error ? error.message : String(error);
49
+ }
50
+ function createJobAbortController(workflowPlugin, runningJobs, job, execution) {
51
+ const controller = new AbortController();
52
+ let timeoutGuard = null;
53
+ const abort = (reason) => {
54
+ if (!controller.signal.aborted) {
55
+ if ((0, import_plugin_workflow.isWorkflowTimeoutError)(reason)) {
56
+ controller.abort(reason);
57
+ } else if (reason === import_plugin_workflow.EXECUTION_REASON.TIMEOUT) {
58
+ controller.abort(new import_plugin_workflow.WorkflowTimeoutError());
59
+ } else {
60
+ controller.abort(reason instanceof Error ? reason : new Error("Workflow execution has been aborted"));
61
+ }
62
+ }
63
+ };
64
+ const remaining = execution.expiresAt ? execution.expiresAt.getTime() - Date.now() : null;
65
+ if (remaining != null) {
66
+ if (remaining <= 0) {
67
+ abort();
68
+ } else {
69
+ timeoutGuard = setTimeout(abort, remaining);
70
+ }
71
+ }
72
+ const unregister = workflowPlugin.registerRunningExecution(execution.id, abort);
73
+ const unregisterJob = runningJobs.register({
74
+ jobId: job.id,
75
+ executionId: execution.id,
76
+ abort
77
+ });
78
+ return {
79
+ abort,
80
+ signal: controller.signal,
81
+ dispose: () => {
82
+ if (timeoutGuard) {
83
+ clearTimeout(timeoutGuard);
84
+ timeoutGuard = null;
85
+ }
86
+ unregister();
87
+ unregisterJob();
88
+ }
89
+ };
90
+ }
91
+ class TaskConsumer {
92
+ constructor(workflowPlugin, runningJobs) {
93
+ this.workflowPlugin = workflowPlugin;
94
+ this.runningJobs = runningJobs;
95
+ }
96
+ ready = false;
97
+ closing = false;
98
+ processing = /* @__PURE__ */ new Set();
99
+ setReady(ready) {
100
+ this.ready = ready;
101
+ }
102
+ idle() {
103
+ return this.ready && !this.closing && this.workflowPlugin.serving();
104
+ }
105
+ async beforeStop() {
106
+ this.closing = true;
107
+ await Promise.allSettled(Array.from(this.processing));
108
+ }
109
+ process = async (message) => {
110
+ if (!isTaskMessage(message)) {
111
+ this.workflowPlugin.getLogger("javascript").warn("invalid JavaScript queue job message ignored", { message });
112
+ return;
113
+ }
114
+ const processing = this.processWithPermit(message.jobId);
115
+ this.processing.add(processing);
116
+ try {
117
+ await processing;
118
+ } finally {
119
+ this.processing.delete(processing);
120
+ }
121
+ };
122
+ async processWithPermit(jobId) {
123
+ if (this.closing) {
124
+ return;
125
+ }
126
+ const existingJob = await this.workflowPlugin.db.getRepository("jobs").findOne({
127
+ filterByTk: jobId
128
+ });
129
+ if (!existingJob) {
130
+ return;
131
+ }
132
+ if (existingJob.status !== import_plugin_workflow.JOB_STATUS.PENDING) {
133
+ return;
134
+ }
135
+ const node = await existingJob.getNode();
136
+ if ((node == null ? void 0 : node.type) !== import_constants.SCRIPT_INSTRUCTION_TYPE) {
137
+ return;
138
+ }
139
+ const claimedJob = await this.claimJob(existingJob);
140
+ if (!claimedJob) {
141
+ return;
142
+ }
143
+ const job = claimedJob;
144
+ const config = node.config;
145
+ const payload = {
146
+ content: (config == null ? void 0 : config.content) ?? "",
147
+ args: getJavaScriptArguments(job),
148
+ timeout: config == null ? void 0 : config.timeout,
149
+ continue: config == null ? void 0 : config.continue
150
+ };
151
+ const execution = await job.getExecution({ attributes: ["id", "workflowId", "status"] });
152
+ if (!execution || execution.status !== import_plugin_workflow.EXECUTION_STATUS.STARTED) {
153
+ await this.finishClaimedJob(job, {
154
+ status: import_plugin_workflow.JOB_STATUS.ABORTED,
155
+ result: `Execution (${job.executionId}) is not running`
156
+ });
157
+ return;
158
+ }
159
+ const logger = this.workflowPlugin.getLogger((execution == null ? void 0 : execution.workflowId) ?? "javascript");
160
+ const queueWaitMs = job.startedAt && job.createdAt ? job.startedAt.getTime() - job.createdAt.getTime() : 0;
161
+ logger.info(`JavaScript job (${job.id}) claimed for node (${job.nodeId})`, {
162
+ executionId: job.executionId,
163
+ jobId: job.id,
164
+ nodeId: job.nodeId,
165
+ queueWaitMs
166
+ });
167
+ if (!await this.workflowPlugin.timeoutManager.shouldContinue(execution)) {
168
+ await this.finishClaimedJob(job, {
169
+ status: import_plugin_workflow.JOB_STATUS.ABORTED,
170
+ result: "Execution timeout reached before JavaScript Worker started"
171
+ });
172
+ return;
173
+ }
174
+ const abortHandle = createJobAbortController(this.workflowPlugin, this.runningJobs, job, execution);
175
+ const stopHeartbeat = this.startClaimHeartbeat(job, abortHandle.abort);
176
+ let values;
177
+ try {
178
+ const result = await import_ScriptWorkerRunner.ScriptWorkerRunner.run(payload.content, payload.args, {
179
+ timeout: payload.timeout,
180
+ logger,
181
+ signal: abortHandle.signal
182
+ });
183
+ if (result.status === import_plugin_workflow.JOB_STATUS.RESOLVED) {
184
+ logger.info(`script (#${job.nodeId}) get result success`);
185
+ values = { status: import_plugin_workflow.JOB_STATUS.RESOLVED, result: result.result };
186
+ logger.info(`run script execution success, node id: ${job.nodeId},the result is ${result.result}`);
187
+ } else if (payload.continue) {
188
+ logger.warn(`script (#${job.nodeId}) get result failed, the reason is ${result.result}`);
189
+ values = { status: import_plugin_workflow.JOB_STATUS.RESOLVED, result: result.result };
190
+ } else {
191
+ logger.info(`script (#${job.nodeId}) get result failed, the reason is ${result.result}`);
192
+ values = { status: import_plugin_workflow.JOB_STATUS.ERROR, result: result.result };
193
+ }
194
+ } catch (error) {
195
+ const message = getErrorMessage(error);
196
+ logger.error(`script (#${job.nodeId}) get result failed, the reason is ${message}`);
197
+ values = {
198
+ status: abortHandle.signal.aborted || (0, import_plugin_workflow.isWorkflowTimeoutError)(error) ? import_plugin_workflow.JOB_STATUS.ABORTED : import_plugin_workflow.JOB_STATUS.ERROR,
199
+ result: message
200
+ };
201
+ } finally {
202
+ abortHandle.dispose();
203
+ await stopHeartbeat();
204
+ }
205
+ try {
206
+ await this.settleClaimedJob(job, execution, values, logger);
207
+ } catch (error) {
208
+ logger.error(`JavaScript job (${job.id}) failed to resume workflow execution (${job.executionId})`, { error });
209
+ throw error;
210
+ }
211
+ }
212
+ async claimJob(job) {
213
+ if (job.startedAt != null) {
214
+ return null;
215
+ }
216
+ const now = /* @__PURE__ */ new Date();
217
+ const JobModel = this.workflowPlugin.db.getModel("jobs");
218
+ const [affected] = await JobModel.update(
219
+ {
220
+ startedAt: now
221
+ },
222
+ {
223
+ where: {
224
+ id: job.id,
225
+ status: import_plugin_workflow.JOB_STATUS.PENDING,
226
+ startedAt: null
227
+ }
228
+ }
229
+ );
230
+ if (!affected) {
231
+ return null;
232
+ }
233
+ const claimedJob = await job.reload();
234
+ return claimedJob;
235
+ }
236
+ async finishClaimedJob(job, values) {
237
+ const updated = await this.updatePendingJob(job, values);
238
+ if (updated) {
239
+ await this.workflowPlugin.resume(job);
240
+ }
241
+ }
242
+ async settleClaimedJob(job, execution, values, logger) {
243
+ if (!await this.workflowPlugin.timeoutManager.shouldContinue(execution)) {
244
+ logger.warn(`script (#${job.nodeId}) result discarded because execution (${execution.id}) is ended`);
245
+ return;
246
+ }
247
+ await execution.reload();
248
+ await job.reload();
249
+ if (execution.status !== import_plugin_workflow.EXECUTION_STATUS.STARTED || job.status !== import_plugin_workflow.JOB_STATUS.PENDING) {
250
+ logger.warn(`script (#${job.nodeId}) result discarded because execution (${execution.id}) is ended`);
251
+ return;
252
+ }
253
+ const updated = await this.updatePendingJob(job, values);
254
+ if (updated) {
255
+ await this.workflowPlugin.resume(job);
256
+ }
257
+ }
258
+ async updatePendingJob(job, values) {
259
+ const JobModel = this.workflowPlugin.db.getModel("jobs");
260
+ const [affected] = await JobModel.update(
261
+ {
262
+ status: values.status,
263
+ result: values.result
264
+ },
265
+ {
266
+ where: {
267
+ id: job.id,
268
+ status: import_plugin_workflow.JOB_STATUS.PENDING,
269
+ startedAt: job.startedAt
270
+ }
271
+ }
272
+ );
273
+ if (!affected) {
274
+ return false;
275
+ }
276
+ await job.reload();
277
+ return true;
278
+ }
279
+ startClaimHeartbeat(job, abort) {
280
+ let heartbeat = null;
281
+ const beat = () => {
282
+ if (heartbeat) {
283
+ return;
284
+ }
285
+ const JobModel = this.workflowPlugin.db.getModel("jobs");
286
+ heartbeat = JobModel.update(
287
+ { updatedAt: /* @__PURE__ */ new Date() },
288
+ {
289
+ where: {
290
+ id: job.id,
291
+ status: import_plugin_workflow.JOB_STATUS.PENDING,
292
+ startedAt: job.startedAt
293
+ }
294
+ }
295
+ ).then(([affected]) => {
296
+ if (!affected) {
297
+ abort();
298
+ }
299
+ }).catch((error) => {
300
+ this.workflowPlugin.getLogger("javascript").error(`JavaScript job (${job.id}) claim heartbeat failed`, {
301
+ error
302
+ });
303
+ }).finally(() => {
304
+ heartbeat = null;
305
+ });
306
+ };
307
+ const timer = setInterval(beat, 3e4);
308
+ return async () => {
309
+ clearInterval(timer);
310
+ await heartbeat;
311
+ };
312
+ }
313
+ }
314
+ // Annotate the CommonJS export names for ESM import in node:
315
+ 0 && (module.exports = {
316
+ TaskConsumer
317
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import type WorkflowPlugin from '@nocobase/plugin-workflow';
10
+ export declare class TaskRecovery {
11
+ private readonly workflowPlugin;
12
+ private readonly taskQueueChannel;
13
+ private timer;
14
+ private ready;
15
+ private recovering;
16
+ constructor(workflowPlugin: WorkflowPlugin, taskQueueChannel: string);
17
+ start(): void;
18
+ stop(): Promise<void>;
19
+ recover(): void;
20
+ private recoverTasks;
21
+ private republishQueuedJobs;
22
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var TaskRecovery_exports = {};
28
+ __export(TaskRecovery_exports, {
29
+ TaskRecovery: () => TaskRecovery
30
+ });
31
+ module.exports = __toCommonJS(TaskRecovery_exports);
32
+ var import_sequelize = require("sequelize");
33
+ var import_plugin_workflow = require("@nocobase/plugin-workflow");
34
+ var import_constants = require("../common/constants");
35
+ const RECOVERY_BATCH_SIZE = 100;
36
+ const RECOVERY_MAX_SCAN_SIZE = 1e3;
37
+ const RECOVERY_INTERVAL = 6e4;
38
+ const RECOVERY_GRACE_PERIOD = RECOVERY_INTERVAL;
39
+ class TaskRecovery {
40
+ constructor(workflowPlugin, taskQueueChannel) {
41
+ this.workflowPlugin = workflowPlugin;
42
+ this.taskQueueChannel = taskQueueChannel;
43
+ }
44
+ timer = null;
45
+ ready = false;
46
+ recovering = null;
47
+ start() {
48
+ this.ready = true;
49
+ this.recover();
50
+ this.timer = setInterval(() => {
51
+ this.recover();
52
+ }, RECOVERY_INTERVAL);
53
+ }
54
+ async stop() {
55
+ var _a;
56
+ this.ready = false;
57
+ if (this.timer) {
58
+ clearInterval(this.timer);
59
+ this.timer = null;
60
+ }
61
+ await ((_a = this.recovering) == null ? void 0 : _a.catch(() => void 0));
62
+ }
63
+ recover() {
64
+ if (!this.ready || this.recovering) {
65
+ return;
66
+ }
67
+ const recovering = this.recoverTasks().catch(() => void 0).finally(() => {
68
+ if (this.recovering === recovering) {
69
+ this.recovering = null;
70
+ }
71
+ });
72
+ this.recovering = recovering;
73
+ }
74
+ async recoverTasks() {
75
+ const logger = this.workflowPlugin.getLogger("javascript");
76
+ if (!this.workflowPlugin.serving()) {
77
+ logger.warn("workflow:process is not serving on this instance, JavaScript job recovery will be ignored");
78
+ return;
79
+ }
80
+ try {
81
+ await this.republishQueuedJobs();
82
+ } catch (error) {
83
+ logger.error("JavaScript job recovery failed", { error });
84
+ }
85
+ }
86
+ async republishQueuedJobs() {
87
+ let cursor = null;
88
+ let scanned = 0;
89
+ const now = /* @__PURE__ */ new Date();
90
+ const recoverableBefore = new Date(now.getTime() - RECOVERY_GRACE_PERIOD);
91
+ const staleBefore = new Date(now.getTime() - 12e4);
92
+ const logger = this.workflowPlugin.getLogger("javascript");
93
+ const JobModel = this.workflowPlugin.db.getModel("jobs");
94
+ while (scanned < RECOVERY_MAX_SCAN_SIZE) {
95
+ const jobs = await JobModel.findAll({
96
+ where: {
97
+ ...cursor == null ? {} : { id: { [import_sequelize.Op.gt]: cursor } },
98
+ status: import_plugin_workflow.JOB_STATUS.PENDING,
99
+ createdAt: {
100
+ [import_sequelize.Op.lt]: recoverableBefore
101
+ }
102
+ },
103
+ attributes: ["id", "startedAt", "updatedAt"],
104
+ include: [
105
+ {
106
+ association: "node",
107
+ attributes: [],
108
+ where: {
109
+ type: import_constants.SCRIPT_INSTRUCTION_TYPE
110
+ },
111
+ required: true
112
+ },
113
+ {
114
+ association: "execution",
115
+ attributes: [],
116
+ where: {
117
+ status: import_plugin_workflow.EXECUTION_STATUS.STARTED,
118
+ [import_sequelize.Op.or]: [
119
+ { expiresAt: null },
120
+ {
121
+ expiresAt: {
122
+ [import_sequelize.Op.gt]: now
123
+ }
124
+ }
125
+ ]
126
+ },
127
+ required: true
128
+ }
129
+ ],
130
+ order: [["id", "ASC"]],
131
+ limit: Math.min(RECOVERY_BATCH_SIZE, RECOVERY_MAX_SCAN_SIZE - scanned)
132
+ });
133
+ if (!jobs.length) {
134
+ break;
135
+ }
136
+ for (const job of jobs) {
137
+ if (job.startedAt) {
138
+ if (job.updatedAt && job.updatedAt <= staleBefore) {
139
+ logger.warn(`JavaScript job (${job.id}) claim heartbeat timed out, manual recovery is required`);
140
+ }
141
+ continue;
142
+ }
143
+ await this.workflowPlugin.app.eventQueue.publish(this.taskQueueChannel, {
144
+ jobId: job.id
145
+ });
146
+ }
147
+ scanned += jobs.length;
148
+ cursor = jobs[jobs.length - 1].id;
149
+ }
150
+ }
151
+ }
152
+ // Annotate the CommonJS export names for ESM import in node:
153
+ 0 && (module.exports = {
154
+ TaskRecovery
155
+ });
package/dist/server/Vm.js CHANGED
@@ -105,7 +105,14 @@ async function main() {
105
105
  const result = script.runInNewContext(context, { timeout: options.timeout });
106
106
  return result;
107
107
  }
108
- main().then((result) => {
108
+ function flushOutput() {
109
+ const flush = (stream) => new Promise((resolve, reject) => {
110
+ stream.write("", (error) => error ? reject(error) : resolve());
111
+ });
112
+ return Promise.all([flush(process.stdout), flush(process.stderr)]);
113
+ }
114
+ main().then(async (result) => {
115
+ await flushOutput();
109
116
  parentPort.postMessage({ type: "result", result });
110
117
  }).catch((error) => {
111
118
  throw error;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ export declare const PENDING_JAVASCRIPT_TASK_CHANNEL = "@nocobase/plugin-workflow-javascript.pendingJavaScriptTask";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var constants_exports = {};
28
+ __export(constants_exports, {
29
+ PENDING_JAVASCRIPT_TASK_CHANNEL: () => PENDING_JAVASCRIPT_TASK_CHANNEL
30
+ });
31
+ module.exports = __toCommonJS(constants_exports);
32
+ const PENDING_JAVASCRIPT_TASK_CHANNEL = "@nocobase/plugin-workflow-javascript.pendingJavaScriptTask";
33
+ // Annotate the CommonJS export names for ESM import in node:
34
+ 0 && (module.exports = {
35
+ PENDING_JAVASCRIPT_TASK_CHANNEL
36
+ });
@@ -1,8 +1,22 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { Plugin } from '@nocobase/server';
2
10
  export declare class PluginWorkflowScriptServer extends Plugin {
11
+ private readonly runningJobs;
12
+ private workflowPlugin;
13
+ private taskConsumer;
14
+ private taskRecovery;
15
+ private readonly handleJobsAfterBulkUpdate;
3
16
  afterAdd(): Promise<void>;
4
17
  beforeLoad(): Promise<void>;
5
18
  load(): Promise<void>;
19
+ handleSyncMessage(message: unknown): Promise<void>;
6
20
  install(): Promise<void>;
7
21
  afterEnable(): Promise<void>;
8
22
  afterDisable(): Promise<void>;
@@ -42,15 +42,105 @@ __export(plugin_exports, {
42
42
  module.exports = __toCommonJS(plugin_exports);
43
43
  var import_server = require("@nocobase/server");
44
44
  var import_plugin_workflow = __toESM(require("@nocobase/plugin-workflow"));
45
+ var import_constants = require("../common/constants");
45
46
  var import_ScriptInstruction = __toESM(require("./ScriptInstruction"));
47
+ var import_constants2 = require("./constants");
48
+ var import_RunningJobs = require("./RunningJobs");
49
+ var import_TaskConsumer = require("./TaskConsumer");
50
+ var import_TaskRecovery = require("./TaskRecovery");
46
51
  class PluginWorkflowScriptServer extends import_server.Plugin {
52
+ runningJobs = new import_RunningJobs.RunningJobs();
53
+ workflowPlugin;
54
+ taskConsumer;
55
+ taskRecovery;
56
+ handleJobsAfterBulkUpdate = async (options) => {
57
+ var _a;
58
+ if (((_a = options.attributes) == null ? void 0 : _a.status) !== import_plugin_workflow.JOB_STATUS.ABORTED) {
59
+ return;
60
+ }
61
+ const JobModel = this.db.getModel("jobs");
62
+ const jobs = await JobModel.findAll({
63
+ attributes: ["id", "executionId"],
64
+ where: options.where,
65
+ include: [
66
+ {
67
+ association: "node",
68
+ attributes: [],
69
+ where: {
70
+ type: import_constants.SCRIPT_INSTRUCTION_TYPE
71
+ },
72
+ required: true
73
+ },
74
+ {
75
+ association: "execution",
76
+ attributes: ["reason"],
77
+ required: false
78
+ }
79
+ ],
80
+ transaction: options.transaction
81
+ });
82
+ const abortJobs = async () => {
83
+ var _a2;
84
+ for (const job of jobs) {
85
+ try {
86
+ await this.runningJobs.abortAcrossInstances(this.workflowPlugin, {
87
+ type: import_RunningJobs.ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE,
88
+ jobId: job.id,
89
+ executionId: job.executionId,
90
+ reason: ((_a2 = job.execution) == null ? void 0 : _a2.reason) ?? void 0
91
+ });
92
+ } catch (error) {
93
+ this.workflowPlugin.getLogger("javascript").error(`aborting JavaScript job (${job.id}) failed`, { error });
94
+ }
95
+ }
96
+ };
97
+ if (options.transaction) {
98
+ options.transaction.afterCommit(abortJobs);
99
+ return;
100
+ }
101
+ await abortJobs();
102
+ };
47
103
  async afterAdd() {
48
104
  }
49
105
  async beforeLoad() {
50
106
  }
51
107
  async load() {
52
- const workflowPlugin = this.app.pm.get(import_plugin_workflow.default);
53
- workflowPlugin.registerInstruction("script", import_ScriptInstruction.default);
108
+ this.workflowPlugin = this.app.pm.get(import_plugin_workflow.default);
109
+ this.taskConsumer = new import_TaskConsumer.TaskConsumer(this.workflowPlugin, this.runningJobs);
110
+ this.taskRecovery = new import_TaskRecovery.TaskRecovery(this.workflowPlugin, import_constants2.PENDING_JAVASCRIPT_TASK_CHANNEL);
111
+ this.workflowPlugin.registerInstruction(
112
+ import_constants.SCRIPT_INSTRUCTION_TYPE,
113
+ new import_ScriptInstruction.default(this.workflowPlugin, this.runningJobs)
114
+ );
115
+ this.db.on("jobs.afterBulkUpdate", this.handleJobsAfterBulkUpdate);
116
+ const workerConcurrency = Number.parseInt(process.env.WORKFLOW_SCRIPT_WORKER_CONCURRENCY, 10);
117
+ this.app.eventQueue.subscribe(import_constants2.PENDING_JAVASCRIPT_TASK_CHANNEL, {
118
+ concurrency: Number.isInteger(workerConcurrency) && workerConcurrency >= 0 ? workerConcurrency : 0,
119
+ idle: () => this.taskConsumer.idle(),
120
+ process: this.taskConsumer.process
121
+ });
122
+ this.app.on("afterStart", () => {
123
+ this.taskConsumer.setReady(true);
124
+ this.taskRecovery.start();
125
+ });
126
+ this.app.on("beforeStop", async () => {
127
+ this.taskConsumer.setReady(false);
128
+ await this.taskConsumer.beforeStop();
129
+ await this.taskRecovery.stop();
130
+ this.db.off("jobs.afterBulkUpdate", this.handleJobsAfterBulkUpdate);
131
+ });
132
+ }
133
+ async handleSyncMessage(message) {
134
+ if (typeof message !== "object" || message === null) {
135
+ return;
136
+ }
137
+ const { type, jobId, reason } = message;
138
+ if (type !== import_RunningJobs.ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE) {
139
+ return;
140
+ }
141
+ if (typeof jobId === "string" || typeof jobId === "number") {
142
+ this.runningJobs.abortJob(jobId, typeof reason === "string" ? reason : void 0);
143
+ }
54
144
  }
55
145
  async install() {
56
146
  }