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.
@@ -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
+ }