biz-a-cli 2.3.78 → 2.3.79-15211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hub.js +86 -50
- package/bin/hubEvent.js +6 -70
- package/bin/migrate.js +12 -10
- package/db/db.js +2 -0
- package/db/ds.js +85 -3
- package/engine/bpm/bpm-agent.js +372 -0
- package/engine/bpm/workflow-runtime.js +393 -21
- package/engine/bpm/workflow.js +1 -1
- package/engine/domain/system.js +74 -0
- package/migrations/1777727892577__bpm.sql +1 -1
- package/package.json +1 -1
- package/readme.md +26 -1
- package/scheduler/datalib.js +6 -1
- package/worker/cliScriptWorker.js +85 -1
- package/worker/cliWorkerPool.js +16 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeEffect,
|
|
3
|
+
executeTimerJob,
|
|
4
|
+
executeBackgroundTransition,
|
|
5
|
+
} from "./workflow-runtime.js";
|
|
6
|
+
import {
|
|
7
|
+
queryData,
|
|
8
|
+
save as dbSave,
|
|
9
|
+
executeBlock,
|
|
10
|
+
getGlobalConfig,
|
|
11
|
+
} from "../../db/db.js";
|
|
12
|
+
import dayjs from "dayjs";
|
|
13
|
+
|
|
14
|
+
export function initBpmAgent(broker, services = "") {
|
|
15
|
+
const servicesHandleByAgent = services
|
|
16
|
+
.split(",")
|
|
17
|
+
.map((e) => e.trim().toUpperCase());
|
|
18
|
+
if (
|
|
19
|
+
!servicesHandleByAgent.includes("BPM") &&
|
|
20
|
+
!servicesHandleByAgent.includes("ALL")
|
|
21
|
+
) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`[BPM Agent] Initialized. Listening for smart triggers...`);
|
|
26
|
+
|
|
27
|
+
let isPollingEffects = false;
|
|
28
|
+
let isPollingTimers = false;
|
|
29
|
+
|
|
30
|
+
// ==========================================
|
|
31
|
+
// EVENT LISTENERS (The "Bus" Stops)
|
|
32
|
+
// ==========================================
|
|
33
|
+
broker.on("sys:tick", () => {
|
|
34
|
+
pollEffects();
|
|
35
|
+
pollTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
broker.on("bpm:externalTransition", async (payload) => {
|
|
39
|
+
try {
|
|
40
|
+
const { instanceId, transitionCode, entityData = {} } = payload;
|
|
41
|
+
const apiConfig = getGlobalConfig();
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
`[BPM Agent] Received External Transition Intent '${transitionCode}' for Instance ${instanceId}`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await executeBackgroundTransition(
|
|
48
|
+
{
|
|
49
|
+
instanceId: instanceId,
|
|
50
|
+
transitionCode: transitionCode,
|
|
51
|
+
entityData: entityData,
|
|
52
|
+
apiConfig: apiConfig,
|
|
53
|
+
},
|
|
54
|
+
"EXTERNAL_EVENT",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log(
|
|
58
|
+
`[BPM Agent] External Transition '${transitionCode}' executed successfully.`,
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(
|
|
62
|
+
`[BPM Agent] External Transition Failed:`,
|
|
63
|
+
error.message,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
broker.on("bpm:effect", async (payload, ackCallback) => {
|
|
69
|
+
if (!payload || (!payload.id && !payload.ID)) {
|
|
70
|
+
pollEffects();
|
|
71
|
+
if (typeof ackCallback === "function")
|
|
72
|
+
ackCallback({ success: true, message: "Polling triggered" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await consumeEffect(payload, ackCallback);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
broker.on("bpm:timerjob", async (payload, ackCallback) => {
|
|
79
|
+
if (!payload || (!payload.id && !payload.ID)) {
|
|
80
|
+
pollTimers();
|
|
81
|
+
if (typeof ackCallback === "function")
|
|
82
|
+
ackCallback({ success: true, message: "Polling triggered" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await consumeTimer(payload, ackCallback);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ==========================================
|
|
89
|
+
// PRODUCERS (Database Pollers)
|
|
90
|
+
// ==========================================
|
|
91
|
+
async function pollEffects() {
|
|
92
|
+
if (isPollingEffects) return;
|
|
93
|
+
isPollingEffects = true;
|
|
94
|
+
try {
|
|
95
|
+
const effectQuery = {
|
|
96
|
+
length: 50,
|
|
97
|
+
columns: [
|
|
98
|
+
{ data: "SYS$BPM_EFFECT_LOG.ID", key: "id" },
|
|
99
|
+
{
|
|
100
|
+
data: "SYS$BPM_EFFECT_LOG.SYS$BPM_WF_INSTANCE_ID",
|
|
101
|
+
key: "sys$bpm_wf_instance_id",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
data: "SYS$BPM_EFFECT_LOG.EFFECT_TYPE",
|
|
105
|
+
key: "effect_type",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
data: "SYS$BPM_EFFECT_LOG.EVENT_NAME",
|
|
109
|
+
key: "event_name",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
data: "SYS$BPM_EFFECT_LOG.PAYLOAD_EVALUATED",
|
|
113
|
+
key: "payload_evaluated",
|
|
114
|
+
},
|
|
115
|
+
{ data: "SYS$BPM_EFFECT_LOG.STATUS", key: "status" },
|
|
116
|
+
{
|
|
117
|
+
data: "SYS$BPM_EFFECT_LOG.RETRY_COUNT",
|
|
118
|
+
key: "retry_count",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
data: "(CAST(SYS$BPM_EFFECT_LOG.EXECUTED_AT AS VARCHAR(24)))",
|
|
122
|
+
key: "executed_at",
|
|
123
|
+
alias: "executed_at",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
filter: [
|
|
127
|
+
// {
|
|
128
|
+
// junction: "",
|
|
129
|
+
// column: "SYS$BPM_EFFECT_LOG.STATUS",
|
|
130
|
+
// operator: "in",
|
|
131
|
+
// value1: "('PENDING', 'RETRYING')",
|
|
132
|
+
// },
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
junction: "",
|
|
136
|
+
column: "SYS$BPM_EFFECT_LOG.STATUS",
|
|
137
|
+
operator: "=",
|
|
138
|
+
value1: "'PENDING'",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
junction: "OR",
|
|
142
|
+
column: "SYS$BPM_EFFECT_LOG.STATUS",
|
|
143
|
+
operator: "=",
|
|
144
|
+
value1: "'RETRYING'",
|
|
145
|
+
},
|
|
146
|
+
// Filter out RETRYING records that haven't waited 60 seconds yet
|
|
147
|
+
{
|
|
148
|
+
junction: "AND",
|
|
149
|
+
column: "SYS$BPM_EFFECT_LOG.EXECUTED_AT",
|
|
150
|
+
operator: "<=",
|
|
151
|
+
value1: "DATEADD(-60 SECOND TO CURRENT_TIMESTAMP)",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const pendingEffects = (await queryData(effectQuery)) || [];
|
|
156
|
+
|
|
157
|
+
for (const rowData of pendingEffects) {
|
|
158
|
+
await consumeEffect(rowData, (ackResult) => {
|
|
159
|
+
if (
|
|
160
|
+
!ackResult.success &&
|
|
161
|
+
ackResult.reason !== "LOCKED_BY_OTHER"
|
|
162
|
+
) {
|
|
163
|
+
console.log(
|
|
164
|
+
`[BPM Agent] Effect ${rowData.id} failed:`,
|
|
165
|
+
ackResult.error,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
console.error("[BPM Agent] Effect poll error:", e.message);
|
|
172
|
+
} finally {
|
|
173
|
+
isPollingEffects = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function pollTimers() {
|
|
178
|
+
if (isPollingTimers) return;
|
|
179
|
+
isPollingTimers = true;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const nowStr = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
|
183
|
+
const timerQuery = {
|
|
184
|
+
length: 50,
|
|
185
|
+
columns: [
|
|
186
|
+
{ data: "SYS$BPM_TIMER_JOB.ID", key: "id" },
|
|
187
|
+
{
|
|
188
|
+
data: "SYS$BPM_TIMER_JOB.SYS$BPM_WF_INSTANCE_ID",
|
|
189
|
+
key: "sys$bpm_wf_instance_id",
|
|
190
|
+
},
|
|
191
|
+
{ data: "SYS$BPM_TIMER_JOB.STATE_CODE", key: "state_code" },
|
|
192
|
+
{ data: "SYS$BPM_TIMER_JOB.TIMER_TYPE", key: "timer_type" },
|
|
193
|
+
],
|
|
194
|
+
filter: [
|
|
195
|
+
{
|
|
196
|
+
junction: "",
|
|
197
|
+
column: "SYS$BPM_TIMER_JOB.STATUS",
|
|
198
|
+
operator: "=",
|
|
199
|
+
value1: "'OPEN'",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
junction: "AND",
|
|
203
|
+
column: "SYS$BPM_TIMER_JOB.DUE_AT",
|
|
204
|
+
operator: "<=",
|
|
205
|
+
value1: `'${nowStr}'`,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
const dueTimers = (await queryData(timerQuery)) || [];
|
|
210
|
+
|
|
211
|
+
for (const rowData of dueTimers) {
|
|
212
|
+
await consumeTimer(rowData, (ackResult) => {
|
|
213
|
+
if (
|
|
214
|
+
!ackResult.success &&
|
|
215
|
+
ackResult.reason !== "LOCKED_BY_OTHER"
|
|
216
|
+
) {
|
|
217
|
+
console.log(
|
|
218
|
+
`[BPM Agent] Timer ${rowData.id} failed:`,
|
|
219
|
+
ackResult.error,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error("[BPM Agent] Timer poll error:", e.message);
|
|
226
|
+
} finally {
|
|
227
|
+
isPollingTimers = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ==========================================
|
|
232
|
+
// CONSUMERS (Executors)
|
|
233
|
+
// ==========================================
|
|
234
|
+
async function consumeEffect(payload, ackCallback) {
|
|
235
|
+
async function finalizeEffect(
|
|
236
|
+
id,
|
|
237
|
+
isSuccess,
|
|
238
|
+
payload,
|
|
239
|
+
errorMessage = "",
|
|
240
|
+
) {
|
|
241
|
+
if (isSuccess) {
|
|
242
|
+
await dbSave({
|
|
243
|
+
SYS$BPM_EFFECT_LOG: {
|
|
244
|
+
id,
|
|
245
|
+
status: "SUCCESS",
|
|
246
|
+
executed_at: new Date(),
|
|
247
|
+
error_message: "",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
const currentRetry =
|
|
252
|
+
parseInt(payload.retry_count || payload.RETRY_COUNT, 10) ||
|
|
253
|
+
0;
|
|
254
|
+
const newRetryCount = currentRetry + 1;
|
|
255
|
+
const nextStatus = newRetryCount < 3 ? "RETRYING" : "FAILED";
|
|
256
|
+
|
|
257
|
+
await dbSave({
|
|
258
|
+
SYS$BPM_EFFECT_LOG: {
|
|
259
|
+
id,
|
|
260
|
+
status: nextStatus,
|
|
261
|
+
executed_at: new Date(),
|
|
262
|
+
retry_count: newRetryCount,
|
|
263
|
+
error_message: errorMessage,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const effectId = Number(payload.id || payload.ID);
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const lockSql = `
|
|
273
|
+
EXECUTE BLOCK RETURNS (CLAIMED INTEGER) AS
|
|
274
|
+
BEGIN
|
|
275
|
+
CLAIMED = 0;
|
|
276
|
+
UPDATE SYS$BPM_EFFECT_LOG SET STATUS = 'PROCESSING'
|
|
277
|
+
WHERE ID = ${effectId} AND STATUS IN ('PENDING', 'RETRYING');
|
|
278
|
+
IF (ROW_COUNT > 0) THEN CLAIMED = 1;
|
|
279
|
+
SUSPEND;
|
|
280
|
+
END
|
|
281
|
+
`;
|
|
282
|
+
const lockRes = await executeBlock(lockSql);
|
|
283
|
+
const lockData = lockRes?.data || lockRes;
|
|
284
|
+
|
|
285
|
+
if (!lockData || !lockData[0] || lockData[0].CLAIMED === 0) {
|
|
286
|
+
if (typeof ackCallback === "function")
|
|
287
|
+
ackCallback({
|
|
288
|
+
success: false,
|
|
289
|
+
reason: "LOCKED_BY_OTHER",
|
|
290
|
+
id: effectId,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const engineResult = await executeEffect(payload);
|
|
296
|
+
|
|
297
|
+
await finalizeEffect(
|
|
298
|
+
effectId,
|
|
299
|
+
engineResult.success,
|
|
300
|
+
payload,
|
|
301
|
+
engineResult.error,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (typeof ackCallback === "function") ackCallback(engineResult);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
try {
|
|
307
|
+
await finalizeEffect(effectId, false, payload, e.message);
|
|
308
|
+
} catch (dbErr) {
|
|
309
|
+
console.error(
|
|
310
|
+
`[BPM Agent] FATAL: Could not revert Effect ${effectId}`,
|
|
311
|
+
dbErr.message,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (typeof ackCallback === "function")
|
|
315
|
+
ackCallback({ success: false, error: e.message });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function consumeTimer(payload, ackCallback) {
|
|
320
|
+
async function finalizeTimer(id, isSuccess) {
|
|
321
|
+
const dbPayload = isSuccess
|
|
322
|
+
? { id, status: "FIRED", fired_at: new Date() }
|
|
323
|
+
: { id, status: "OPEN" };
|
|
324
|
+
|
|
325
|
+
await dbSave({ SYS$BPM_TIMER_JOB: dbPayload });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const timerId = Number(payload.id || payload.ID);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const lockSql = `
|
|
332
|
+
EXECUTE BLOCK RETURNS (CLAIMED INTEGER) AS
|
|
333
|
+
BEGIN
|
|
334
|
+
CLAIMED = 0;
|
|
335
|
+
UPDATE SYS$BPM_TIMER_JOB SET STATUS = 'PROCESSING'
|
|
336
|
+
WHERE ID = ${timerId} AND STATUS = 'OPEN';
|
|
337
|
+
IF (ROW_COUNT > 0) THEN CLAIMED = 1;
|
|
338
|
+
SUSPEND;
|
|
339
|
+
END
|
|
340
|
+
`;
|
|
341
|
+
const lockRes = await executeBlock(lockSql);
|
|
342
|
+
const lockData = lockRes?.data || lockRes;
|
|
343
|
+
|
|
344
|
+
if (!lockData || !lockData[0] || lockData[0].CLAIMED === 0) {
|
|
345
|
+
if (typeof ackCallback === "function")
|
|
346
|
+
ackCallback({
|
|
347
|
+
success: false,
|
|
348
|
+
reason: "LOCKED_BY_OTHER",
|
|
349
|
+
id: timerId,
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const engineResult = await executeTimerJob(payload);
|
|
355
|
+
|
|
356
|
+
await finalizeTimer(timerId, engineResult.success);
|
|
357
|
+
|
|
358
|
+
if (typeof ackCallback === "function") ackCallback(engineResult);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
try {
|
|
361
|
+
await finalizeTimer(timerId, false);
|
|
362
|
+
} catch (dbErr) {
|
|
363
|
+
console.error(
|
|
364
|
+
`[BPM Agent] FATAL: Could not revert Timer ${timerId} to OPEN`,
|
|
365
|
+
dbErr.message,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (typeof ackCallback === "function")
|
|
369
|
+
ackCallback({ success: false, error: e.message });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|