@tasker-systems/tasker 0.1.0-alpha.1
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/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +1642 -0
- package/dist/events/index.js.map +1 -0
- package/dist/ffi/index.d.ts +308 -0
- package/dist/ffi/index.js +1945 -0
- package/dist/ffi/index.js.map +1 -0
- package/dist/index-B3BcknlZ.d.ts +1349 -0
- package/dist/index.d.ts +4013 -0
- package/dist/index.js +7870 -0
- package/dist/index.js.map +1 -0
- package/dist/koffi-3HFAASOB.node +0 -0
- package/dist/koffi-AHHUCM3C.node +0 -0
- package/dist/koffi-AVDVVSXH.node +0 -0
- package/dist/koffi-BMO5K7B3.node +0 -0
- package/dist/koffi-G4D35B2D.node +0 -0
- package/dist/koffi-GG4SDSYA.node +0 -0
- package/dist/koffi-GOENU54R.node +0 -0
- package/dist/koffi-IDX6JEDH.node +0 -0
- package/dist/koffi-KFZAXWPQ.node +0 -0
- package/dist/koffi-LOH6WKRQ.node +0 -0
- package/dist/koffi-LUY2JHJP.node +0 -0
- package/dist/koffi-OMHWL3D6.node +0 -0
- package/dist/koffi-QKY2KSXW.node +0 -0
- package/dist/koffi-ROB3FRHA.node +0 -0
- package/dist/koffi-SE4ZI36U.node +0 -0
- package/dist/koffi-X3YT67KE.node +0 -0
- package/dist/koffi-X7JMBSZH.node +0 -0
- package/dist/koffi-YNQDUF3Q.node +0 -0
- package/dist/runtime-interface-CE4viUt7.d.ts +694 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1642 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
|
|
4
|
+
// src/events/event-emitter.ts
|
|
5
|
+
|
|
6
|
+
// src/events/event-names.ts
|
|
7
|
+
var StepEventNames = {
|
|
8
|
+
/** Emitted when a step execution event is received from the FFI layer */
|
|
9
|
+
STEP_EXECUTION_RECEIVED: "step.execution.received",
|
|
10
|
+
/** Emitted when a step handler starts executing */
|
|
11
|
+
STEP_EXECUTION_STARTED: "step.execution.started",
|
|
12
|
+
/** Emitted when a step handler completes successfully */
|
|
13
|
+
STEP_EXECUTION_COMPLETED: "step.execution.completed",
|
|
14
|
+
/** Emitted when a step handler fails */
|
|
15
|
+
STEP_EXECUTION_FAILED: "step.execution.failed",
|
|
16
|
+
/** Emitted when a step completion is sent back to Rust */
|
|
17
|
+
STEP_COMPLETION_SENT: "step.completion.sent",
|
|
18
|
+
/** Emitted when a step handler times out */
|
|
19
|
+
STEP_EXECUTION_TIMEOUT: "step.execution.timeout",
|
|
20
|
+
/** TAS-125: Emitted when a checkpoint yield is sent back to Rust */
|
|
21
|
+
STEP_CHECKPOINT_YIELD_SENT: "step.checkpoint_yield.sent"
|
|
22
|
+
};
|
|
23
|
+
var WorkerEventNames = {
|
|
24
|
+
/** Emitted when the worker starts up */
|
|
25
|
+
WORKER_STARTED: "worker.started",
|
|
26
|
+
/** Emitted when the worker is ready to process events */
|
|
27
|
+
WORKER_READY: "worker.ready",
|
|
28
|
+
/** Emitted when graceful shutdown begins */
|
|
29
|
+
WORKER_SHUTDOWN_STARTED: "worker.shutdown.started",
|
|
30
|
+
/** Emitted when the worker has fully stopped */
|
|
31
|
+
WORKER_STOPPED: "worker.stopped",
|
|
32
|
+
/** Emitted when the worker encounters an error */
|
|
33
|
+
WORKER_ERROR: "worker.error"
|
|
34
|
+
};
|
|
35
|
+
var PollerEventNames = {
|
|
36
|
+
/** Emitted when the poller starts */
|
|
37
|
+
POLLER_STARTED: "poller.started",
|
|
38
|
+
/** Emitted when the poller stops */
|
|
39
|
+
POLLER_STOPPED: "poller.stopped",
|
|
40
|
+
/** Emitted when a poll cycle completes */
|
|
41
|
+
POLLER_CYCLE_COMPLETE: "poller.cycle.complete",
|
|
42
|
+
/** Emitted when starvation is detected */
|
|
43
|
+
POLLER_STARVATION_DETECTED: "poller.starvation.detected",
|
|
44
|
+
/** Emitted when the poller encounters an error */
|
|
45
|
+
POLLER_ERROR: "poller.error"
|
|
46
|
+
};
|
|
47
|
+
var MetricsEventNames = {
|
|
48
|
+
/** Emitted periodically with FFI dispatch metrics */
|
|
49
|
+
METRICS_UPDATED: "metrics.updated",
|
|
50
|
+
/** Emitted when metrics collection fails */
|
|
51
|
+
METRICS_ERROR: "metrics.error"
|
|
52
|
+
};
|
|
53
|
+
var EventNames = {
|
|
54
|
+
...StepEventNames,
|
|
55
|
+
...WorkerEventNames,
|
|
56
|
+
...PollerEventNames,
|
|
57
|
+
...MetricsEventNames
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/events/event-emitter.ts
|
|
61
|
+
var TaskerEventEmitter = class extends EventEmitter {
|
|
62
|
+
instanceId;
|
|
63
|
+
constructor() {
|
|
64
|
+
super();
|
|
65
|
+
this.instanceId = crypto.randomUUID();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the unique instance ID for this emitter
|
|
69
|
+
*/
|
|
70
|
+
getInstanceId() {
|
|
71
|
+
return this.instanceId;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Emit a step execution received event
|
|
75
|
+
*/
|
|
76
|
+
emitStepReceived(event) {
|
|
77
|
+
this.emit(StepEventNames.STEP_EXECUTION_RECEIVED, {
|
|
78
|
+
event,
|
|
79
|
+
receivedAt: /* @__PURE__ */ new Date()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Emit a step execution started event
|
|
84
|
+
*/
|
|
85
|
+
emitStepStarted(eventId, stepUuid, taskUuid, handlerName) {
|
|
86
|
+
this.emit(StepEventNames.STEP_EXECUTION_STARTED, {
|
|
87
|
+
eventId,
|
|
88
|
+
stepUuid,
|
|
89
|
+
taskUuid,
|
|
90
|
+
handlerName,
|
|
91
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit a step execution completed event
|
|
96
|
+
*/
|
|
97
|
+
emitStepCompleted(eventId, stepUuid, taskUuid, result, executionTimeMs) {
|
|
98
|
+
this.emit(StepEventNames.STEP_EXECUTION_COMPLETED, {
|
|
99
|
+
eventId,
|
|
100
|
+
stepUuid,
|
|
101
|
+
taskUuid,
|
|
102
|
+
result,
|
|
103
|
+
executionTimeMs,
|
|
104
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emit a step execution failed event
|
|
109
|
+
*/
|
|
110
|
+
emitStepFailed(eventId, stepUuid, taskUuid, error) {
|
|
111
|
+
this.emit(StepEventNames.STEP_EXECUTION_FAILED, {
|
|
112
|
+
eventId,
|
|
113
|
+
stepUuid,
|
|
114
|
+
taskUuid,
|
|
115
|
+
error,
|
|
116
|
+
failedAt: /* @__PURE__ */ new Date()
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Emit a step completion sent event
|
|
121
|
+
*/
|
|
122
|
+
emitCompletionSent(eventId, stepUuid, success) {
|
|
123
|
+
this.emit(StepEventNames.STEP_COMPLETION_SENT, {
|
|
124
|
+
eventId,
|
|
125
|
+
stepUuid,
|
|
126
|
+
success,
|
|
127
|
+
sentAt: /* @__PURE__ */ new Date()
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Emit a worker started event
|
|
132
|
+
*/
|
|
133
|
+
emitWorkerStarted(workerId) {
|
|
134
|
+
this.emit(WorkerEventNames.WORKER_STARTED, {
|
|
135
|
+
workerId,
|
|
136
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
137
|
+
message: "Worker started"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Emit a worker ready event
|
|
142
|
+
*/
|
|
143
|
+
emitWorkerReady(workerId) {
|
|
144
|
+
this.emit(WorkerEventNames.WORKER_READY, {
|
|
145
|
+
workerId,
|
|
146
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
147
|
+
message: "Worker ready to process events"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Emit a worker shutdown started event
|
|
152
|
+
*/
|
|
153
|
+
emitWorkerShutdownStarted(workerId) {
|
|
154
|
+
this.emit(WorkerEventNames.WORKER_SHUTDOWN_STARTED, {
|
|
155
|
+
workerId,
|
|
156
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
157
|
+
message: "Graceful shutdown initiated"
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Emit a worker stopped event
|
|
162
|
+
*/
|
|
163
|
+
emitWorkerStopped(workerId) {
|
|
164
|
+
this.emit(WorkerEventNames.WORKER_STOPPED, {
|
|
165
|
+
workerId,
|
|
166
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
167
|
+
message: "Worker stopped"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Emit a worker error event
|
|
172
|
+
*/
|
|
173
|
+
emitWorkerError(error, context) {
|
|
174
|
+
this.emit(WorkerEventNames.WORKER_ERROR, {
|
|
175
|
+
error,
|
|
176
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
177
|
+
context
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Emit a metrics updated event
|
|
182
|
+
*/
|
|
183
|
+
emitMetricsUpdated(metrics) {
|
|
184
|
+
this.emit(MetricsEventNames.METRICS_UPDATED, {
|
|
185
|
+
metrics,
|
|
186
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Emit a starvation detected event
|
|
191
|
+
*/
|
|
192
|
+
emitStarvationDetected(metrics) {
|
|
193
|
+
this.emit(PollerEventNames.POLLER_STARVATION_DETECTED, {
|
|
194
|
+
metrics,
|
|
195
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var loggerOptions = {
|
|
200
|
+
name: "event-poller",
|
|
201
|
+
level: process.env.RUST_LOG ?? "info"
|
|
202
|
+
};
|
|
203
|
+
if (process.env.TASKER_ENV !== "production") {
|
|
204
|
+
loggerOptions.transport = {
|
|
205
|
+
target: "pino-pretty",
|
|
206
|
+
options: { colorize: true }
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
var log = pino(loggerOptions);
|
|
210
|
+
var EventPoller = class {
|
|
211
|
+
runtime;
|
|
212
|
+
config;
|
|
213
|
+
emitter;
|
|
214
|
+
state = "stopped";
|
|
215
|
+
pollCount = 0;
|
|
216
|
+
cycleCount = 0;
|
|
217
|
+
intervalId = null;
|
|
218
|
+
stepEventCallback = null;
|
|
219
|
+
errorCallback = null;
|
|
220
|
+
metricsCallback = null;
|
|
221
|
+
/**
|
|
222
|
+
* Create a new EventPoller.
|
|
223
|
+
*
|
|
224
|
+
* @param runtime - The FFI runtime for polling events
|
|
225
|
+
* @param emitter - The event emitter to dispatch events to (required, no fallback)
|
|
226
|
+
* @param config - Optional configuration for polling behavior
|
|
227
|
+
*/
|
|
228
|
+
constructor(runtime, emitter, config = {}) {
|
|
229
|
+
this.runtime = runtime;
|
|
230
|
+
this.emitter = emitter;
|
|
231
|
+
this.config = {
|
|
232
|
+
pollIntervalMs: config.pollIntervalMs ?? 10,
|
|
233
|
+
starvationCheckInterval: config.starvationCheckInterval ?? 100,
|
|
234
|
+
cleanupInterval: config.cleanupInterval ?? 1e3,
|
|
235
|
+
metricsInterval: config.metricsInterval ?? 100,
|
|
236
|
+
maxEventsPerCycle: config.maxEventsPerCycle ?? 100
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the current poller state
|
|
241
|
+
*/
|
|
242
|
+
getState() {
|
|
243
|
+
return this.state;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if the poller is running
|
|
247
|
+
*/
|
|
248
|
+
isRunning() {
|
|
249
|
+
return this.state === "running";
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the total number of polls executed
|
|
253
|
+
*/
|
|
254
|
+
getPollCount() {
|
|
255
|
+
return this.pollCount;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get the total number of cycles completed
|
|
259
|
+
*/
|
|
260
|
+
getCycleCount() {
|
|
261
|
+
return this.cycleCount;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Register a callback for step events
|
|
265
|
+
*/
|
|
266
|
+
onStepEvent(callback) {
|
|
267
|
+
this.stepEventCallback = callback;
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Register a callback for errors
|
|
272
|
+
*/
|
|
273
|
+
onError(callback) {
|
|
274
|
+
this.errorCallback = callback;
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Register a callback for metrics
|
|
279
|
+
*/
|
|
280
|
+
onMetrics(callback) {
|
|
281
|
+
this.metricsCallback = callback;
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Start the polling loop
|
|
286
|
+
*/
|
|
287
|
+
start() {
|
|
288
|
+
log.info(
|
|
289
|
+
{ component: "event-poller", operation: "start", currentState: this.state },
|
|
290
|
+
"EventPoller start() called"
|
|
291
|
+
);
|
|
292
|
+
if (this.state === "running") {
|
|
293
|
+
log.debug({ component: "event-poller" }, "Already running, returning early");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
log.debug(
|
|
297
|
+
{ component: "event-poller", runtimeLoaded: this.runtime.isLoaded },
|
|
298
|
+
"Checking runtime.isLoaded"
|
|
299
|
+
);
|
|
300
|
+
if (!this.runtime.isLoaded) {
|
|
301
|
+
throw new Error("Runtime not loaded. Call runtime.load() first.");
|
|
302
|
+
}
|
|
303
|
+
this.state = "running";
|
|
304
|
+
this.pollCount = 0;
|
|
305
|
+
this.cycleCount = 0;
|
|
306
|
+
this.emitter.emit(PollerEventNames.POLLER_STARTED, {
|
|
307
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
308
|
+
message: "Event poller started"
|
|
309
|
+
});
|
|
310
|
+
log.info(
|
|
311
|
+
{ component: "event-poller", intervalMs: this.config.pollIntervalMs },
|
|
312
|
+
"Setting up setInterval for polling"
|
|
313
|
+
);
|
|
314
|
+
this.intervalId = setInterval(() => {
|
|
315
|
+
this.poll();
|
|
316
|
+
}, this.config.pollIntervalMs);
|
|
317
|
+
log.info(
|
|
318
|
+
{ component: "event-poller", intervalId: String(this.intervalId) },
|
|
319
|
+
"setInterval created, polling active"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Stop the polling loop
|
|
324
|
+
*/
|
|
325
|
+
async stop() {
|
|
326
|
+
if (this.state === "stopped") {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.state = "stopping";
|
|
330
|
+
if (this.intervalId !== null) {
|
|
331
|
+
clearInterval(this.intervalId);
|
|
332
|
+
this.intervalId = null;
|
|
333
|
+
}
|
|
334
|
+
this.state = "stopped";
|
|
335
|
+
this.emitter.emit(PollerEventNames.POLLER_STOPPED, {
|
|
336
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
337
|
+
message: `Event poller stopped after ${this.pollCount} polls`
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Execute a single poll cycle
|
|
342
|
+
*/
|
|
343
|
+
poll() {
|
|
344
|
+
if (this.pollCount === 0) {
|
|
345
|
+
log.info(
|
|
346
|
+
{ component: "event-poller", state: this.state },
|
|
347
|
+
"First poll() call - setInterval is working"
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (this.pollCount % 100 === 0) {
|
|
351
|
+
log.debug(
|
|
352
|
+
{ component: "event-poller", pollCount: this.pollCount, state: this.state },
|
|
353
|
+
"poll() cycle"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (this.state !== "running") {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.pollCount++;
|
|
360
|
+
try {
|
|
361
|
+
let eventsProcessed = 0;
|
|
362
|
+
for (let i = 0; i < this.config.maxEventsPerCycle; i++) {
|
|
363
|
+
const event = this.runtime.pollStepEvents();
|
|
364
|
+
if (event === null) {
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
eventsProcessed++;
|
|
368
|
+
const handlerCallable = event.step_definition.handler.callable;
|
|
369
|
+
log.info(
|
|
370
|
+
{
|
|
371
|
+
component: "event-poller",
|
|
372
|
+
operation: "event_received",
|
|
373
|
+
stepUuid: event.step_uuid,
|
|
374
|
+
handlerCallable,
|
|
375
|
+
eventIndex: i
|
|
376
|
+
},
|
|
377
|
+
`Received step event for handler: ${handlerCallable}`
|
|
378
|
+
);
|
|
379
|
+
this.handleStepEvent(event);
|
|
380
|
+
}
|
|
381
|
+
if (this.pollCount % this.config.starvationCheckInterval === 0) {
|
|
382
|
+
this.checkStarvation();
|
|
383
|
+
}
|
|
384
|
+
if (this.pollCount % this.config.cleanupInterval === 0) {
|
|
385
|
+
this.runtime.cleanupTimeouts();
|
|
386
|
+
}
|
|
387
|
+
if (this.pollCount % this.config.metricsInterval === 0) {
|
|
388
|
+
this.emitMetrics();
|
|
389
|
+
}
|
|
390
|
+
this.cycleCount++;
|
|
391
|
+
if (eventsProcessed > 0) {
|
|
392
|
+
this.emitter.emit(PollerEventNames.POLLER_CYCLE_COMPLETE, {
|
|
393
|
+
eventsProcessed,
|
|
394
|
+
cycleNumber: this.cycleCount,
|
|
395
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Handle a step event
|
|
404
|
+
*/
|
|
405
|
+
handleStepEvent(event) {
|
|
406
|
+
const handlerCallable = event.step_definition.handler.callable;
|
|
407
|
+
log.debug(
|
|
408
|
+
{
|
|
409
|
+
component: "event-poller",
|
|
410
|
+
operation: "handle_step_event",
|
|
411
|
+
stepUuid: event.step_uuid,
|
|
412
|
+
handlerCallable,
|
|
413
|
+
hasCallback: !!this.stepEventCallback
|
|
414
|
+
},
|
|
415
|
+
"Handling step event"
|
|
416
|
+
);
|
|
417
|
+
const listenerCountBefore = this.emitter.listenerCount(StepEventNames.STEP_EXECUTION_RECEIVED);
|
|
418
|
+
log.info(
|
|
419
|
+
{
|
|
420
|
+
component: "event-poller",
|
|
421
|
+
emitterInstanceId: this.emitter.getInstanceId(),
|
|
422
|
+
stepUuid: event.step_uuid,
|
|
423
|
+
listenerCount: listenerCountBefore,
|
|
424
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
425
|
+
},
|
|
426
|
+
`About to emit ${StepEventNames.STEP_EXECUTION_RECEIVED} event`
|
|
427
|
+
);
|
|
428
|
+
try {
|
|
429
|
+
const emitResult = this.emitter.emit(StepEventNames.STEP_EXECUTION_RECEIVED, {
|
|
430
|
+
event,
|
|
431
|
+
receivedAt: /* @__PURE__ */ new Date()
|
|
432
|
+
});
|
|
433
|
+
log.info(
|
|
434
|
+
{
|
|
435
|
+
component: "event-poller",
|
|
436
|
+
stepUuid: event.step_uuid,
|
|
437
|
+
emitResult,
|
|
438
|
+
listenerCountAfter: this.emitter.listenerCount(StepEventNames.STEP_EXECUTION_RECEIVED),
|
|
439
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
440
|
+
},
|
|
441
|
+
`Emit returned: ${emitResult} (true means listeners were called)`
|
|
442
|
+
);
|
|
443
|
+
} catch (emitError) {
|
|
444
|
+
log.error(
|
|
445
|
+
{
|
|
446
|
+
component: "event-poller",
|
|
447
|
+
stepUuid: event.step_uuid,
|
|
448
|
+
error: emitError instanceof Error ? emitError.message : String(emitError),
|
|
449
|
+
stack: emitError instanceof Error ? emitError.stack : void 0
|
|
450
|
+
},
|
|
451
|
+
"Error during emit"
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (this.stepEventCallback) {
|
|
455
|
+
log.debug(
|
|
456
|
+
{ component: "event-poller", stepUuid: event.step_uuid },
|
|
457
|
+
"Invoking step event callback"
|
|
458
|
+
);
|
|
459
|
+
this.stepEventCallback(event).catch((error) => {
|
|
460
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)));
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
log.warn(
|
|
464
|
+
{ component: "event-poller", stepUuid: event.step_uuid },
|
|
465
|
+
"No step event callback registered!"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Check for starvation conditions
|
|
471
|
+
*/
|
|
472
|
+
checkStarvation() {
|
|
473
|
+
try {
|
|
474
|
+
this.runtime.checkStarvationWarnings();
|
|
475
|
+
const metrics = this.runtime.getFfiDispatchMetrics();
|
|
476
|
+
if (metrics.starvation_detected) {
|
|
477
|
+
this.emitter.emitStarvationDetected(metrics);
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Emit current metrics
|
|
485
|
+
*/
|
|
486
|
+
emitMetrics() {
|
|
487
|
+
try {
|
|
488
|
+
const metrics = this.runtime.getFfiDispatchMetrics();
|
|
489
|
+
this.emitter.emitMetricsUpdated(metrics);
|
|
490
|
+
if (this.metricsCallback) {
|
|
491
|
+
this.metricsCallback(metrics);
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this.emitter.emit(MetricsEventNames.METRICS_ERROR, {
|
|
495
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
496
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Handle an error
|
|
502
|
+
*/
|
|
503
|
+
handleError(error) {
|
|
504
|
+
this.emitter.emit(PollerEventNames.POLLER_ERROR, {
|
|
505
|
+
error,
|
|
506
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
507
|
+
});
|
|
508
|
+
if (this.errorCallback) {
|
|
509
|
+
this.errorCallback(error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
function createEventPoller(runtime, emitter, config) {
|
|
514
|
+
return new EventPoller(runtime, emitter, config);
|
|
515
|
+
}
|
|
516
|
+
function getLoggingRuntime() {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
function toFfiFields(fields) {
|
|
520
|
+
if (!fields) {
|
|
521
|
+
return {};
|
|
522
|
+
}
|
|
523
|
+
const result = {};
|
|
524
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
525
|
+
if (value !== void 0) {
|
|
526
|
+
result[key] = value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
function fallbackLog(level, message, fields) {
|
|
532
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
533
|
+
const fieldsStr = fields ? ` ${JSON.stringify(fields)}` : "";
|
|
534
|
+
console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}${fieldsStr}`);
|
|
535
|
+
}
|
|
536
|
+
function logError(message, fields) {
|
|
537
|
+
const runtime = getLoggingRuntime();
|
|
538
|
+
if (!runtime) {
|
|
539
|
+
fallbackLog("error", message, fields);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
runtime.logError(message, toFfiFields(fields));
|
|
544
|
+
} catch {
|
|
545
|
+
fallbackLog("error", message, fields);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function logWarn(message, fields) {
|
|
549
|
+
const runtime = getLoggingRuntime();
|
|
550
|
+
if (!runtime) {
|
|
551
|
+
fallbackLog("warn", message, fields);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
runtime.logWarn(message, toFfiFields(fields));
|
|
556
|
+
} catch {
|
|
557
|
+
fallbackLog("warn", message, fields);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function logInfo(message, fields) {
|
|
561
|
+
const runtime = getLoggingRuntime();
|
|
562
|
+
if (!runtime) {
|
|
563
|
+
fallbackLog("info", message, fields);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
runtime.logInfo(message, toFfiFields(fields));
|
|
568
|
+
} catch {
|
|
569
|
+
fallbackLog("info", message, fields);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function logDebug(message, fields) {
|
|
573
|
+
const runtime = getLoggingRuntime();
|
|
574
|
+
if (!runtime) {
|
|
575
|
+
fallbackLog("debug", message, fields);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
runtime.logDebug(message, toFfiFields(fields));
|
|
580
|
+
} catch {
|
|
581
|
+
fallbackLog("debug", message, fields);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/types/step-context.ts
|
|
586
|
+
var StepContext = class _StepContext {
|
|
587
|
+
/** The original FFI step event */
|
|
588
|
+
event;
|
|
589
|
+
/** Task UUID */
|
|
590
|
+
taskUuid;
|
|
591
|
+
/** Step UUID */
|
|
592
|
+
stepUuid;
|
|
593
|
+
/** Correlation ID for tracing */
|
|
594
|
+
correlationId;
|
|
595
|
+
/** Name of the handler being executed */
|
|
596
|
+
handlerName;
|
|
597
|
+
/** Input data for the handler (from task context) */
|
|
598
|
+
inputData;
|
|
599
|
+
/** Results from dependent steps */
|
|
600
|
+
dependencyResults;
|
|
601
|
+
/** Handler-specific configuration (from step_definition.handler.initialization) */
|
|
602
|
+
stepConfig;
|
|
603
|
+
/** Step-specific inputs (from workflow_step.inputs, used for batch cursor config) */
|
|
604
|
+
stepInputs;
|
|
605
|
+
/** Current retry attempt number */
|
|
606
|
+
retryCount;
|
|
607
|
+
/** Maximum retry attempts allowed */
|
|
608
|
+
maxRetries;
|
|
609
|
+
constructor(params) {
|
|
610
|
+
this.event = params.event;
|
|
611
|
+
this.taskUuid = params.taskUuid;
|
|
612
|
+
this.stepUuid = params.stepUuid;
|
|
613
|
+
this.correlationId = params.correlationId;
|
|
614
|
+
this.handlerName = params.handlerName;
|
|
615
|
+
this.inputData = params.inputData;
|
|
616
|
+
this.dependencyResults = params.dependencyResults;
|
|
617
|
+
this.stepConfig = params.stepConfig;
|
|
618
|
+
this.stepInputs = params.stepInputs;
|
|
619
|
+
this.retryCount = params.retryCount;
|
|
620
|
+
this.maxRetries = params.maxRetries;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Create a StepContext from an FFI event.
|
|
624
|
+
*
|
|
625
|
+
* Extracts input data, dependency results, and configuration from
|
|
626
|
+
* the task_sequence_step payload.
|
|
627
|
+
*
|
|
628
|
+
* The FFI data structure mirrors the Ruby TaskSequenceStepWrapper:
|
|
629
|
+
* - task.context -> inputData (task context with user inputs)
|
|
630
|
+
* - dependency_results -> results from parent steps
|
|
631
|
+
* - step_definition.handler.initialization -> stepConfig
|
|
632
|
+
* - workflow_step.attempts -> retryCount
|
|
633
|
+
* - workflow_step.max_attempts -> maxRetries
|
|
634
|
+
* - workflow_step.inputs -> stepInputs
|
|
635
|
+
*
|
|
636
|
+
* @param event - The FFI step event
|
|
637
|
+
* @param handlerName - Name of the handler to execute
|
|
638
|
+
* @returns A StepContext populated from the event
|
|
639
|
+
*/
|
|
640
|
+
static fromFfiEvent(event, handlerName) {
|
|
641
|
+
const task = event.task ?? {};
|
|
642
|
+
const inputData = task.context ?? {};
|
|
643
|
+
const dependencyResults = event.dependency_results ?? {};
|
|
644
|
+
const stepDefinition = event.step_definition ?? {};
|
|
645
|
+
const handlerConfig = stepDefinition.handler ?? {};
|
|
646
|
+
const stepConfig = handlerConfig.initialization ?? {};
|
|
647
|
+
const workflowStep = event.workflow_step ?? {};
|
|
648
|
+
const retryCount = workflowStep.attempts ?? 0;
|
|
649
|
+
const maxRetries = workflowStep.max_attempts ?? 3;
|
|
650
|
+
const stepInputs = workflowStep.inputs ?? {};
|
|
651
|
+
return new _StepContext({
|
|
652
|
+
event,
|
|
653
|
+
taskUuid: event.task_uuid,
|
|
654
|
+
stepUuid: event.step_uuid,
|
|
655
|
+
correlationId: event.correlation_id,
|
|
656
|
+
handlerName,
|
|
657
|
+
inputData,
|
|
658
|
+
dependencyResults,
|
|
659
|
+
stepConfig,
|
|
660
|
+
stepInputs,
|
|
661
|
+
retryCount,
|
|
662
|
+
maxRetries
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get the computed result value from a dependency step.
|
|
667
|
+
*
|
|
668
|
+
* This method extracts the actual computed value from a dependency result,
|
|
669
|
+
* unwrapping any nested structure. Matches Python's get_dependency_result().
|
|
670
|
+
*
|
|
671
|
+
* The dependency result structure can be:
|
|
672
|
+
* - {"result": actual_value} - unwraps to actual_value
|
|
673
|
+
* - primitive value - returns as-is
|
|
674
|
+
*
|
|
675
|
+
* @param stepName - Name of the dependency step
|
|
676
|
+
* @returns The computed result value, or null if not found
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* // Instead of:
|
|
681
|
+
* const step1Result = context.dependencyResults['step_1'] || {};
|
|
682
|
+
* const value = step1Result.result; // Might be nested!
|
|
683
|
+
*
|
|
684
|
+
* // Use:
|
|
685
|
+
* const value = context.getDependencyResult('step_1'); // Unwrapped
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
getDependencyResult(stepName) {
|
|
689
|
+
const resultHash = this.dependencyResults[stepName];
|
|
690
|
+
if (resultHash === void 0 || resultHash === null) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (typeof resultHash === "object" && resultHash !== null && "result" in resultHash) {
|
|
694
|
+
return resultHash.result;
|
|
695
|
+
}
|
|
696
|
+
return resultHash;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Get a value from the input data.
|
|
700
|
+
*
|
|
701
|
+
* @param key - The key to look up in inputData
|
|
702
|
+
* @returns The value or undefined if not found
|
|
703
|
+
*/
|
|
704
|
+
getInput(key) {
|
|
705
|
+
return this.inputData[key];
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Get a value from the step configuration.
|
|
709
|
+
*
|
|
710
|
+
* @param key - The key to look up in stepConfig
|
|
711
|
+
* @returns The value or undefined if not found
|
|
712
|
+
*/
|
|
713
|
+
getConfig(key) {
|
|
714
|
+
return this.stepConfig[key];
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Check if this is a retry attempt.
|
|
718
|
+
*
|
|
719
|
+
* @returns True if retryCount > 0
|
|
720
|
+
*/
|
|
721
|
+
isRetry() {
|
|
722
|
+
return this.retryCount > 0;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Check if this is the last allowed retry attempt.
|
|
726
|
+
*
|
|
727
|
+
* @returns True if retryCount >= maxRetries - 1
|
|
728
|
+
*/
|
|
729
|
+
isLastRetry() {
|
|
730
|
+
return this.retryCount >= this.maxRetries - 1;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Get a value from the input data with a default.
|
|
734
|
+
*
|
|
735
|
+
* @param key - The key to look up in inputData
|
|
736
|
+
* @param defaultValue - Value to return if key not found or undefined
|
|
737
|
+
* @returns The value or default if not found/undefined
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```typescript
|
|
741
|
+
* const batchSize = context.getInputOr('batch_size', 100);
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
getInputOr(key, defaultValue) {
|
|
745
|
+
const value = this.inputData[key];
|
|
746
|
+
return value === void 0 ? defaultValue : value;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Extract a nested field from a dependency result.
|
|
750
|
+
*
|
|
751
|
+
* Useful when dependency results are complex objects and you need
|
|
752
|
+
* to extract a specific nested value without manual object traversal.
|
|
753
|
+
*
|
|
754
|
+
* @param stepName - Name of the dependency step
|
|
755
|
+
* @param path - Path elements to traverse into the result
|
|
756
|
+
* @returns The nested value, or null if not found
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```typescript
|
|
760
|
+
* // Extract nested field from dependency result
|
|
761
|
+
* const csvPath = context.getDependencyField('analyze_csv', 'csv_file_path');
|
|
762
|
+
* // Multiple levels deep
|
|
763
|
+
* const value = context.getDependencyField('step_1', 'data', 'items');
|
|
764
|
+
* ```
|
|
765
|
+
*/
|
|
766
|
+
getDependencyField(stepName, ...path) {
|
|
767
|
+
let result = this.getDependencyResult(stepName);
|
|
768
|
+
if (result === null || result === void 0) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
for (const key of path) {
|
|
772
|
+
if (typeof result !== "object" || result === null) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
result = result[key];
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
778
|
+
}
|
|
779
|
+
// ===========================================================================
|
|
780
|
+
// CHECKPOINT ACCESSORS (TAS-125 Batch Processing Support)
|
|
781
|
+
// ===========================================================================
|
|
782
|
+
/**
|
|
783
|
+
* Get the raw checkpoint data from the workflow step.
|
|
784
|
+
*
|
|
785
|
+
* @returns The checkpoint data object or null if not set
|
|
786
|
+
*/
|
|
787
|
+
get checkpoint() {
|
|
788
|
+
const workflowStep = this.event.workflow_step ?? {};
|
|
789
|
+
return workflowStep.checkpoint ?? null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get the checkpoint cursor position.
|
|
793
|
+
*
|
|
794
|
+
* The cursor represents the current position in batch processing,
|
|
795
|
+
* allowing handlers to resume from where they left off.
|
|
796
|
+
*
|
|
797
|
+
* @returns The cursor value (number, string, or object) or null if not set
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```typescript
|
|
801
|
+
* const cursor = context.checkpointCursor;
|
|
802
|
+
* const startFrom = cursor ?? 0;
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
get checkpointCursor() {
|
|
806
|
+
return this.checkpoint?.cursor ?? null;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get the number of items processed in the current batch run.
|
|
810
|
+
*
|
|
811
|
+
* @returns Number of items processed (0 if no checkpoint)
|
|
812
|
+
*/
|
|
813
|
+
get checkpointItemsProcessed() {
|
|
814
|
+
return this.checkpoint?.items_processed ?? 0;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get the accumulated results from batch processing.
|
|
818
|
+
*
|
|
819
|
+
* Accumulated results allow handlers to maintain running totals
|
|
820
|
+
* or aggregated state across checkpoint boundaries.
|
|
821
|
+
*
|
|
822
|
+
* @returns The accumulated results object or null if not set
|
|
823
|
+
*
|
|
824
|
+
* @example
|
|
825
|
+
* ```typescript
|
|
826
|
+
* const totals = context.accumulatedResults ?? {};
|
|
827
|
+
* const currentSum = totals.sum ?? 0;
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
get accumulatedResults() {
|
|
831
|
+
return this.checkpoint?.accumulated_results ?? null;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Check if a checkpoint exists for this step.
|
|
835
|
+
*
|
|
836
|
+
* @returns True if a checkpoint cursor exists
|
|
837
|
+
*
|
|
838
|
+
* @example
|
|
839
|
+
* ```typescript
|
|
840
|
+
* if (context.hasCheckpoint()) {
|
|
841
|
+
* console.log(`Resuming from cursor: ${context.checkpointCursor}`);
|
|
842
|
+
* }
|
|
843
|
+
* ```
|
|
844
|
+
*/
|
|
845
|
+
hasCheckpoint() {
|
|
846
|
+
return this.checkpointCursor !== null;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Get all dependency result keys.
|
|
850
|
+
*
|
|
851
|
+
* @returns Array of step names that have dependency results
|
|
852
|
+
*/
|
|
853
|
+
getDependencyResultKeys() {
|
|
854
|
+
return Object.keys(this.dependencyResults);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Get all dependency results matching a step name prefix.
|
|
858
|
+
*
|
|
859
|
+
* This is useful for batch processing where multiple worker steps
|
|
860
|
+
* share a common prefix (e.g., "process_batch_001", "process_batch_002").
|
|
861
|
+
*
|
|
862
|
+
* Returns the unwrapped result values (same as getDependencyResult).
|
|
863
|
+
*
|
|
864
|
+
* @param prefix - Step name prefix to match
|
|
865
|
+
* @returns Array of unwrapped result values from matching steps
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* ```typescript
|
|
869
|
+
* // For batch worker results named: process_batch_001, process_batch_002, etc.
|
|
870
|
+
* const batchResults = context.getAllDependencyResults('process_batch_');
|
|
871
|
+
* const total = batchResults.reduce((sum, r) => sum + r.count, 0);
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
getAllDependencyResults(prefix) {
|
|
875
|
+
const results = [];
|
|
876
|
+
for (const key of Object.keys(this.dependencyResults)) {
|
|
877
|
+
if (key.startsWith(prefix)) {
|
|
878
|
+
const result = this.getDependencyResult(key);
|
|
879
|
+
if (result !== null) {
|
|
880
|
+
results.push(result);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return results;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/subscriber/step-execution-subscriber.ts
|
|
889
|
+
var loggerOptions2 = {
|
|
890
|
+
name: "step-subscriber",
|
|
891
|
+
level: process.env.RUST_LOG ?? "info"
|
|
892
|
+
};
|
|
893
|
+
if (process.env.TASKER_ENV !== "production") {
|
|
894
|
+
loggerOptions2.transport = {
|
|
895
|
+
target: "pino-pretty",
|
|
896
|
+
options: { colorize: true }
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
var pinoLog = pino(loggerOptions2);
|
|
900
|
+
var StepExecutionSubscriber = class {
|
|
901
|
+
emitter;
|
|
902
|
+
registry;
|
|
903
|
+
runtime;
|
|
904
|
+
workerId;
|
|
905
|
+
maxConcurrent;
|
|
906
|
+
handlerTimeoutMs;
|
|
907
|
+
running = false;
|
|
908
|
+
activeHandlers = 0;
|
|
909
|
+
processedCount = 0;
|
|
910
|
+
errorCount = 0;
|
|
911
|
+
/**
|
|
912
|
+
* Create a new StepExecutionSubscriber.
|
|
913
|
+
*
|
|
914
|
+
* @param emitter - The event emitter to subscribe to (required, no fallback)
|
|
915
|
+
* @param registry - The handler registry for resolving step handlers
|
|
916
|
+
* @param runtime - The FFI runtime for submitting results (required, no fallback)
|
|
917
|
+
* @param config - Optional configuration for execution behavior
|
|
918
|
+
*/
|
|
919
|
+
constructor(emitter, registry, runtime, config = {}) {
|
|
920
|
+
this.emitter = emitter;
|
|
921
|
+
this.registry = registry;
|
|
922
|
+
this.runtime = runtime;
|
|
923
|
+
this.workerId = config.workerId ?? `typescript-worker-${process.pid}`;
|
|
924
|
+
this.maxConcurrent = config.maxConcurrent ?? 10;
|
|
925
|
+
this.handlerTimeoutMs = config.handlerTimeoutMs ?? 3e5;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Start subscribing to step execution events.
|
|
929
|
+
*/
|
|
930
|
+
start() {
|
|
931
|
+
pinoLog.info(
|
|
932
|
+
{ component: "subscriber", emitterInstanceId: this.emitter.getInstanceId() },
|
|
933
|
+
"StepExecutionSubscriber.start() called"
|
|
934
|
+
);
|
|
935
|
+
if (this.running) {
|
|
936
|
+
logWarn("StepExecutionSubscriber already running", {
|
|
937
|
+
component: "subscriber"
|
|
938
|
+
});
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
this.running = true;
|
|
942
|
+
this.processedCount = 0;
|
|
943
|
+
this.errorCount = 0;
|
|
944
|
+
pinoLog.info(
|
|
945
|
+
{
|
|
946
|
+
component: "subscriber",
|
|
947
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED,
|
|
948
|
+
emitterInstanceId: this.emitter.getInstanceId()
|
|
949
|
+
},
|
|
950
|
+
"Registering event listener on emitter"
|
|
951
|
+
);
|
|
952
|
+
this.emitter.on(
|
|
953
|
+
StepEventNames.STEP_EXECUTION_RECEIVED,
|
|
954
|
+
(payload) => {
|
|
955
|
+
try {
|
|
956
|
+
pinoLog.info(
|
|
957
|
+
{
|
|
958
|
+
component: "subscriber",
|
|
959
|
+
eventId: payload.event.event_id,
|
|
960
|
+
stepUuid: payload.event.step_uuid
|
|
961
|
+
},
|
|
962
|
+
"Received step event in subscriber callback!"
|
|
963
|
+
);
|
|
964
|
+
pinoLog.info({ component: "subscriber" }, "About to call handleEvent from callback");
|
|
965
|
+
this.handleEvent(payload.event);
|
|
966
|
+
pinoLog.info({ component: "subscriber" }, "handleEvent returned from callback");
|
|
967
|
+
} catch (error) {
|
|
968
|
+
pinoLog.error(
|
|
969
|
+
{
|
|
970
|
+
component: "subscriber",
|
|
971
|
+
error: error instanceof Error ? error.message : String(error),
|
|
972
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
973
|
+
},
|
|
974
|
+
"EXCEPTION in event listener callback!"
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
pinoLog.info(
|
|
980
|
+
{ component: "subscriber", workerId: this.workerId },
|
|
981
|
+
"StepExecutionSubscriber started successfully"
|
|
982
|
+
);
|
|
983
|
+
logInfo("StepExecutionSubscriber started", {
|
|
984
|
+
component: "subscriber",
|
|
985
|
+
operation: "start",
|
|
986
|
+
worker_id: this.workerId
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Stop subscribing to step execution events.
|
|
991
|
+
*
|
|
992
|
+
* Note: Does not wait for in-flight handlers to complete.
|
|
993
|
+
* Use waitForCompletion() if you need to wait.
|
|
994
|
+
*/
|
|
995
|
+
stop() {
|
|
996
|
+
if (!this.running) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
this.running = false;
|
|
1000
|
+
this.emitter.removeAllListeners(StepEventNames.STEP_EXECUTION_RECEIVED);
|
|
1001
|
+
logInfo("StepExecutionSubscriber stopped", {
|
|
1002
|
+
component: "subscriber",
|
|
1003
|
+
operation: "stop",
|
|
1004
|
+
processed_count: String(this.processedCount),
|
|
1005
|
+
error_count: String(this.errorCount)
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Check if the subscriber is running.
|
|
1010
|
+
*/
|
|
1011
|
+
isRunning() {
|
|
1012
|
+
return this.running;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get the count of events processed.
|
|
1016
|
+
*/
|
|
1017
|
+
getProcessedCount() {
|
|
1018
|
+
return this.processedCount;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Get the count of errors encountered.
|
|
1022
|
+
*/
|
|
1023
|
+
getErrorCount() {
|
|
1024
|
+
return this.errorCount;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get the count of currently active handlers.
|
|
1028
|
+
*/
|
|
1029
|
+
getActiveHandlers() {
|
|
1030
|
+
return this.activeHandlers;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Wait for all active handlers to complete.
|
|
1034
|
+
*
|
|
1035
|
+
* @param timeoutMs - Maximum time to wait (default: 30000)
|
|
1036
|
+
* @returns True if all handlers completed, false if timeout
|
|
1037
|
+
*/
|
|
1038
|
+
async waitForCompletion(timeoutMs = 3e4) {
|
|
1039
|
+
const startTime = Date.now();
|
|
1040
|
+
const checkInterval = 100;
|
|
1041
|
+
while (this.activeHandlers > 0) {
|
|
1042
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1043
|
+
logWarn("Timeout waiting for handlers to complete", {
|
|
1044
|
+
component: "subscriber",
|
|
1045
|
+
active_handlers: String(this.activeHandlers)
|
|
1046
|
+
});
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1050
|
+
}
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Handle a step execution event.
|
|
1055
|
+
*/
|
|
1056
|
+
handleEvent(event) {
|
|
1057
|
+
pinoLog.info(
|
|
1058
|
+
{
|
|
1059
|
+
component: "subscriber",
|
|
1060
|
+
eventId: event.event_id,
|
|
1061
|
+
running: this.running,
|
|
1062
|
+
activeHandlers: this.activeHandlers,
|
|
1063
|
+
maxConcurrent: this.maxConcurrent
|
|
1064
|
+
},
|
|
1065
|
+
"handleEvent() called"
|
|
1066
|
+
);
|
|
1067
|
+
if (!this.running) {
|
|
1068
|
+
pinoLog.warn(
|
|
1069
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1070
|
+
"Received event while stopped, ignoring"
|
|
1071
|
+
);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (this.activeHandlers >= this.maxConcurrent) {
|
|
1075
|
+
pinoLog.warn(
|
|
1076
|
+
{
|
|
1077
|
+
component: "subscriber",
|
|
1078
|
+
activeHandlers: this.activeHandlers,
|
|
1079
|
+
maxConcurrent: this.maxConcurrent
|
|
1080
|
+
},
|
|
1081
|
+
"Max concurrent handlers reached, event will be re-polled"
|
|
1082
|
+
);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
pinoLog.info(
|
|
1086
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1087
|
+
"About to call processEvent()"
|
|
1088
|
+
);
|
|
1089
|
+
this.processEvent(event).catch((error) => {
|
|
1090
|
+
pinoLog.error(
|
|
1091
|
+
{
|
|
1092
|
+
component: "subscriber",
|
|
1093
|
+
eventId: event.event_id,
|
|
1094
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1095
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1096
|
+
},
|
|
1097
|
+
"Unhandled error in processEvent"
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Process a step execution event.
|
|
1103
|
+
*/
|
|
1104
|
+
async processEvent(event) {
|
|
1105
|
+
pinoLog.info({ component: "subscriber", eventId: event.event_id }, "processEvent() starting");
|
|
1106
|
+
this.activeHandlers++;
|
|
1107
|
+
const startTime = Date.now();
|
|
1108
|
+
try {
|
|
1109
|
+
const handlerName = this.extractHandlerName(event);
|
|
1110
|
+
pinoLog.info(
|
|
1111
|
+
{ component: "subscriber", eventId: event.event_id, handlerName },
|
|
1112
|
+
"Extracted handler name"
|
|
1113
|
+
);
|
|
1114
|
+
if (!handlerName) {
|
|
1115
|
+
pinoLog.error(
|
|
1116
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1117
|
+
"No handler name found!"
|
|
1118
|
+
);
|
|
1119
|
+
await this.submitErrorResult(event, "No handler name found in step definition", startTime);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
pinoLog.info(
|
|
1123
|
+
{
|
|
1124
|
+
component: "subscriber",
|
|
1125
|
+
eventId: event.event_id,
|
|
1126
|
+
stepUuid: event.step_uuid,
|
|
1127
|
+
handlerName
|
|
1128
|
+
},
|
|
1129
|
+
"Processing step event"
|
|
1130
|
+
);
|
|
1131
|
+
this.emitter.emit(StepEventNames.STEP_EXECUTION_STARTED, {
|
|
1132
|
+
eventId: event.event_id,
|
|
1133
|
+
stepUuid: event.step_uuid,
|
|
1134
|
+
handlerName,
|
|
1135
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1136
|
+
});
|
|
1137
|
+
pinoLog.info({ component: "subscriber", handlerName }, "Resolving handler from registry...");
|
|
1138
|
+
const handler = await this.registry.resolve(handlerName);
|
|
1139
|
+
pinoLog.info(
|
|
1140
|
+
{ component: "subscriber", handlerName, handlerFound: !!handler },
|
|
1141
|
+
"Handler resolution result"
|
|
1142
|
+
);
|
|
1143
|
+
if (!handler) {
|
|
1144
|
+
pinoLog.error({ component: "subscriber", handlerName }, "Handler not found in registry!");
|
|
1145
|
+
await this.submitErrorResult(event, `Handler not found: ${handlerName}`, startTime);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
pinoLog.info({ component: "subscriber", handlerName }, "Creating StepContext from FFI event");
|
|
1149
|
+
const context = StepContext.fromFfiEvent(event, handlerName);
|
|
1150
|
+
pinoLog.info(
|
|
1151
|
+
{ component: "subscriber", handlerName },
|
|
1152
|
+
"StepContext created, executing handler"
|
|
1153
|
+
);
|
|
1154
|
+
const result = await this.executeWithTimeout(
|
|
1155
|
+
() => handler.call(context),
|
|
1156
|
+
this.handlerTimeoutMs
|
|
1157
|
+
);
|
|
1158
|
+
pinoLog.info(
|
|
1159
|
+
{ component: "subscriber", handlerName, success: result.success },
|
|
1160
|
+
"Handler execution completed"
|
|
1161
|
+
);
|
|
1162
|
+
const executionTimeMs = Date.now() - startTime;
|
|
1163
|
+
await this.submitResult(event, result, executionTimeMs);
|
|
1164
|
+
if (result.success) {
|
|
1165
|
+
this.emitter.emit(StepEventNames.STEP_EXECUTION_COMPLETED, {
|
|
1166
|
+
eventId: event.event_id,
|
|
1167
|
+
stepUuid: event.step_uuid,
|
|
1168
|
+
handlerName,
|
|
1169
|
+
executionTimeMs,
|
|
1170
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1171
|
+
});
|
|
1172
|
+
} else {
|
|
1173
|
+
this.emitter.emit(StepEventNames.STEP_EXECUTION_FAILED, {
|
|
1174
|
+
eventId: event.event_id,
|
|
1175
|
+
stepUuid: event.step_uuid,
|
|
1176
|
+
handlerName,
|
|
1177
|
+
error: result.errorMessage,
|
|
1178
|
+
executionTimeMs,
|
|
1179
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
this.processedCount++;
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
this.errorCount++;
|
|
1185
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1186
|
+
logError("Handler execution failed", {
|
|
1187
|
+
component: "subscriber",
|
|
1188
|
+
event_id: event.event_id,
|
|
1189
|
+
step_uuid: event.step_uuid,
|
|
1190
|
+
error_message: errorMessage
|
|
1191
|
+
});
|
|
1192
|
+
await this.submitErrorResult(event, errorMessage, startTime);
|
|
1193
|
+
this.emitter.emit(StepEventNames.STEP_EXECUTION_FAILED, {
|
|
1194
|
+
eventId: event.event_id,
|
|
1195
|
+
stepUuid: event.step_uuid,
|
|
1196
|
+
error: errorMessage,
|
|
1197
|
+
executionTimeMs: Date.now() - startTime,
|
|
1198
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1199
|
+
});
|
|
1200
|
+
} finally {
|
|
1201
|
+
this.activeHandlers--;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Execute a function with a timeout.
|
|
1206
|
+
*/
|
|
1207
|
+
async executeWithTimeout(fn, timeoutMs) {
|
|
1208
|
+
return Promise.race([
|
|
1209
|
+
fn(),
|
|
1210
|
+
new Promise(
|
|
1211
|
+
(_, reject) => setTimeout(
|
|
1212
|
+
() => reject(new Error(`Handler execution timed out after ${timeoutMs}ms`)),
|
|
1213
|
+
timeoutMs
|
|
1214
|
+
)
|
|
1215
|
+
)
|
|
1216
|
+
]);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Extract handler name from FFI event.
|
|
1220
|
+
*
|
|
1221
|
+
* The handler name is in step_definition.handler.callable
|
|
1222
|
+
*/
|
|
1223
|
+
extractHandlerName(event) {
|
|
1224
|
+
const stepDefinition = event.step_definition;
|
|
1225
|
+
if (!stepDefinition) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
const handler = stepDefinition.handler;
|
|
1229
|
+
if (!handler) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return handler.callable || null;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Submit a handler result via FFI.
|
|
1236
|
+
*
|
|
1237
|
+
* TAS-125: Detects checkpoint yields and routes them to checkpointYieldStepEvent
|
|
1238
|
+
* instead of the normal completion path.
|
|
1239
|
+
*/
|
|
1240
|
+
async submitResult(event, result, executionTimeMs) {
|
|
1241
|
+
pinoLog.info(
|
|
1242
|
+
{ component: "subscriber", eventId: event.event_id, runtimeLoaded: this.runtime.isLoaded },
|
|
1243
|
+
"submitResult() called"
|
|
1244
|
+
);
|
|
1245
|
+
if (!this.runtime.isLoaded) {
|
|
1246
|
+
pinoLog.error(
|
|
1247
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1248
|
+
"Cannot submit result: runtime not loaded!"
|
|
1249
|
+
);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (result.metadata?.checkpoint_yield === true) {
|
|
1253
|
+
await this.submitCheckpointYield(event, result);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const executionResult = this.buildExecutionResult(event, result, executionTimeMs);
|
|
1257
|
+
await this.sendCompletionViaFfi(event, executionResult, result.success);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* TAS-125: Submit a checkpoint yield via FFI.
|
|
1261
|
+
*
|
|
1262
|
+
* Called when a handler returns a checkpoint_yield result.
|
|
1263
|
+
* This persists the checkpoint and re-dispatches the step.
|
|
1264
|
+
*/
|
|
1265
|
+
async submitCheckpointYield(event, result) {
|
|
1266
|
+
pinoLog.info(
|
|
1267
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1268
|
+
"submitCheckpointYield() called - handler yielded checkpoint"
|
|
1269
|
+
);
|
|
1270
|
+
const resultData = result.result ?? {};
|
|
1271
|
+
const checkpointData = {
|
|
1272
|
+
step_uuid: event.step_uuid,
|
|
1273
|
+
cursor: resultData.cursor ?? 0,
|
|
1274
|
+
items_processed: resultData.items_processed ?? 0
|
|
1275
|
+
};
|
|
1276
|
+
const accumulatedResults = resultData.accumulated_results;
|
|
1277
|
+
if (accumulatedResults !== void 0) {
|
|
1278
|
+
checkpointData.accumulated_results = accumulatedResults;
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
const success = this.runtime.checkpointYieldStepEvent(event.event_id, checkpointData);
|
|
1282
|
+
if (success) {
|
|
1283
|
+
pinoLog.info(
|
|
1284
|
+
{
|
|
1285
|
+
component: "subscriber",
|
|
1286
|
+
eventId: event.event_id,
|
|
1287
|
+
cursor: checkpointData.cursor,
|
|
1288
|
+
itemsProcessed: checkpointData.items_processed
|
|
1289
|
+
},
|
|
1290
|
+
"Checkpoint yield submitted successfully - step will be re-dispatched"
|
|
1291
|
+
);
|
|
1292
|
+
this.emitter.emit(StepEventNames.STEP_CHECKPOINT_YIELD_SENT, {
|
|
1293
|
+
eventId: event.event_id,
|
|
1294
|
+
stepUuid: event.step_uuid,
|
|
1295
|
+
cursor: checkpointData.cursor,
|
|
1296
|
+
itemsProcessed: checkpointData.items_processed,
|
|
1297
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1298
|
+
});
|
|
1299
|
+
logInfo("Checkpoint yield submitted", {
|
|
1300
|
+
component: "subscriber",
|
|
1301
|
+
event_id: event.event_id,
|
|
1302
|
+
step_uuid: event.step_uuid,
|
|
1303
|
+
cursor: String(checkpointData.cursor),
|
|
1304
|
+
items_processed: String(checkpointData.items_processed)
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
pinoLog.error(
|
|
1308
|
+
{ component: "subscriber", eventId: event.event_id },
|
|
1309
|
+
"Checkpoint yield rejected by Rust - event may not be in pending map"
|
|
1310
|
+
);
|
|
1311
|
+
logError("Checkpoint yield rejected", {
|
|
1312
|
+
component: "subscriber",
|
|
1313
|
+
event_id: event.event_id,
|
|
1314
|
+
step_uuid: event.step_uuid
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
pinoLog.error(
|
|
1319
|
+
{
|
|
1320
|
+
component: "subscriber",
|
|
1321
|
+
eventId: event.event_id,
|
|
1322
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1323
|
+
},
|
|
1324
|
+
"Checkpoint yield failed with error"
|
|
1325
|
+
);
|
|
1326
|
+
logError("Failed to submit checkpoint yield", {
|
|
1327
|
+
component: "subscriber",
|
|
1328
|
+
event_id: event.event_id,
|
|
1329
|
+
error_message: error instanceof Error ? error.message : String(error)
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Submit an error result via FFI (for handler resolution/execution failures).
|
|
1335
|
+
*/
|
|
1336
|
+
async submitErrorResult(event, errorMessage, startTime) {
|
|
1337
|
+
if (!this.runtime.isLoaded) {
|
|
1338
|
+
logError("Cannot submit error result: runtime not available", {
|
|
1339
|
+
component: "subscriber",
|
|
1340
|
+
event_id: event.event_id
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const executionTimeMs = Date.now() - startTime;
|
|
1345
|
+
const executionResult = this.buildErrorExecutionResult(event, errorMessage, executionTimeMs);
|
|
1346
|
+
const accepted = await this.sendCompletionViaFfi(event, executionResult, false);
|
|
1347
|
+
if (accepted) {
|
|
1348
|
+
this.errorCount++;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Build a StepExecutionResult from a handler result.
|
|
1353
|
+
*
|
|
1354
|
+
* IMPORTANT: metadata.retryable must be set for Rust's is_retryable() to work correctly.
|
|
1355
|
+
*/
|
|
1356
|
+
buildExecutionResult(event, result, executionTimeMs) {
|
|
1357
|
+
const executionResult = {
|
|
1358
|
+
step_uuid: event.step_uuid,
|
|
1359
|
+
success: result.success,
|
|
1360
|
+
result: result.result ?? {},
|
|
1361
|
+
metadata: {
|
|
1362
|
+
execution_time_ms: executionTimeMs,
|
|
1363
|
+
worker_id: this.workerId,
|
|
1364
|
+
handler_name: this.extractHandlerName(event) ?? "unknown",
|
|
1365
|
+
attempt_number: event.workflow_step?.attempts ?? 1,
|
|
1366
|
+
retryable: result.retryable ?? false,
|
|
1367
|
+
...result.metadata
|
|
1368
|
+
},
|
|
1369
|
+
status: result.success ? "completed" : "failed"
|
|
1370
|
+
};
|
|
1371
|
+
if (!result.success) {
|
|
1372
|
+
executionResult.error = {
|
|
1373
|
+
message: result.errorMessage ?? "Unknown error",
|
|
1374
|
+
error_type: result.errorType ?? "handler_error",
|
|
1375
|
+
retryable: result.retryable,
|
|
1376
|
+
status_code: null,
|
|
1377
|
+
backtrace: null
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
return executionResult;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Build an error StepExecutionResult for handler resolution/execution failures.
|
|
1384
|
+
*
|
|
1385
|
+
* IMPORTANT: metadata.retryable must be set for Rust's is_retryable() to work correctly.
|
|
1386
|
+
*/
|
|
1387
|
+
buildErrorExecutionResult(event, errorMessage, executionTimeMs) {
|
|
1388
|
+
return {
|
|
1389
|
+
step_uuid: event.step_uuid,
|
|
1390
|
+
success: false,
|
|
1391
|
+
result: {},
|
|
1392
|
+
metadata: {
|
|
1393
|
+
execution_time_ms: executionTimeMs,
|
|
1394
|
+
worker_id: this.workerId,
|
|
1395
|
+
retryable: true
|
|
1396
|
+
},
|
|
1397
|
+
status: "error",
|
|
1398
|
+
error: {
|
|
1399
|
+
message: errorMessage,
|
|
1400
|
+
error_type: "handler_error",
|
|
1401
|
+
retryable: true,
|
|
1402
|
+
status_code: null,
|
|
1403
|
+
backtrace: null
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Send a completion result to Rust via FFI and handle the response.
|
|
1409
|
+
*
|
|
1410
|
+
* @returns true if the completion was accepted by Rust, false otherwise
|
|
1411
|
+
*/
|
|
1412
|
+
async sendCompletionViaFfi(event, executionResult, isSuccess) {
|
|
1413
|
+
pinoLog.info(
|
|
1414
|
+
{
|
|
1415
|
+
component: "subscriber",
|
|
1416
|
+
eventId: event.event_id,
|
|
1417
|
+
stepUuid: event.step_uuid,
|
|
1418
|
+
resultJson: JSON.stringify(executionResult)
|
|
1419
|
+
},
|
|
1420
|
+
"About to call runtime.completeStepEvent()"
|
|
1421
|
+
);
|
|
1422
|
+
try {
|
|
1423
|
+
const ffiResult = this.runtime.completeStepEvent(event.event_id, executionResult);
|
|
1424
|
+
if (ffiResult) {
|
|
1425
|
+
this.handleFfiSuccess(event, executionResult, isSuccess);
|
|
1426
|
+
return true;
|
|
1427
|
+
}
|
|
1428
|
+
this.handleFfiRejection(event);
|
|
1429
|
+
return false;
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
this.handleFfiError(event, error);
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Handle successful FFI completion submission.
|
|
1437
|
+
*/
|
|
1438
|
+
handleFfiSuccess(event, executionResult, isSuccess) {
|
|
1439
|
+
pinoLog.info(
|
|
1440
|
+
{ component: "subscriber", eventId: event.event_id, success: isSuccess },
|
|
1441
|
+
"completeStepEvent() returned TRUE - completion accepted by Rust"
|
|
1442
|
+
);
|
|
1443
|
+
this.emitter.emit(StepEventNames.STEP_COMPLETION_SENT, {
|
|
1444
|
+
eventId: event.event_id,
|
|
1445
|
+
stepUuid: event.step_uuid,
|
|
1446
|
+
success: isSuccess,
|
|
1447
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1448
|
+
});
|
|
1449
|
+
logDebug("Step result submitted", {
|
|
1450
|
+
component: "subscriber",
|
|
1451
|
+
event_id: event.event_id,
|
|
1452
|
+
step_uuid: event.step_uuid,
|
|
1453
|
+
success: String(isSuccess),
|
|
1454
|
+
execution_time_ms: String(executionResult.metadata.execution_time_ms)
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Handle FFI completion rejection (event not in pending map).
|
|
1459
|
+
*/
|
|
1460
|
+
handleFfiRejection(event) {
|
|
1461
|
+
pinoLog.error(
|
|
1462
|
+
{
|
|
1463
|
+
component: "subscriber",
|
|
1464
|
+
eventId: event.event_id,
|
|
1465
|
+
stepUuid: event.step_uuid
|
|
1466
|
+
},
|
|
1467
|
+
"completeStepEvent() returned FALSE - completion REJECTED by Rust! Event may not be in pending map."
|
|
1468
|
+
);
|
|
1469
|
+
logError("FFI completion rejected", {
|
|
1470
|
+
component: "subscriber",
|
|
1471
|
+
event_id: event.event_id,
|
|
1472
|
+
step_uuid: event.step_uuid
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Handle FFI completion error.
|
|
1477
|
+
*/
|
|
1478
|
+
handleFfiError(event, error) {
|
|
1479
|
+
pinoLog.error(
|
|
1480
|
+
{
|
|
1481
|
+
component: "subscriber",
|
|
1482
|
+
eventId: event.event_id,
|
|
1483
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1484
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1485
|
+
},
|
|
1486
|
+
"completeStepEvent() THREW AN ERROR!"
|
|
1487
|
+
);
|
|
1488
|
+
logError("Failed to submit step result", {
|
|
1489
|
+
component: "subscriber",
|
|
1490
|
+
event_id: event.event_id,
|
|
1491
|
+
error_message: error instanceof Error ? error.message : String(error)
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// src/events/event-system.ts
|
|
1497
|
+
var loggerOptions3 = {
|
|
1498
|
+
name: "event-system",
|
|
1499
|
+
level: process.env.RUST_LOG ?? "info"
|
|
1500
|
+
};
|
|
1501
|
+
if (process.env.TASKER_ENV !== "production") {
|
|
1502
|
+
loggerOptions3.transport = {
|
|
1503
|
+
target: "pino-pretty",
|
|
1504
|
+
options: { colorize: true }
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
var log2 = pino(loggerOptions3);
|
|
1508
|
+
var EventSystem = class {
|
|
1509
|
+
emitter;
|
|
1510
|
+
poller;
|
|
1511
|
+
subscriber;
|
|
1512
|
+
running = false;
|
|
1513
|
+
/**
|
|
1514
|
+
* Create a new EventSystem.
|
|
1515
|
+
*
|
|
1516
|
+
* @param runtime - The FFI runtime for polling events and submitting results
|
|
1517
|
+
* @param registry - The handler registry for resolving step handlers
|
|
1518
|
+
* @param config - Optional configuration for poller and subscriber
|
|
1519
|
+
*/
|
|
1520
|
+
constructor(runtime, registry, config = {}) {
|
|
1521
|
+
this.emitter = new TaskerEventEmitter();
|
|
1522
|
+
this.poller = new EventPoller(runtime, this.emitter, config.poller);
|
|
1523
|
+
this.subscriber = new StepExecutionSubscriber(
|
|
1524
|
+
this.emitter,
|
|
1525
|
+
registry,
|
|
1526
|
+
runtime,
|
|
1527
|
+
config.subscriber
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Start the event system.
|
|
1532
|
+
*
|
|
1533
|
+
* Starts the subscriber first (to register listeners), then the poller.
|
|
1534
|
+
* This ensures no events are missed.
|
|
1535
|
+
*/
|
|
1536
|
+
start() {
|
|
1537
|
+
log2.info(
|
|
1538
|
+
{ component: "event-system", emitterInstanceId: this.emitter.getInstanceId() },
|
|
1539
|
+
"EventSystem.start() called"
|
|
1540
|
+
);
|
|
1541
|
+
if (this.running) {
|
|
1542
|
+
log2.warn({ component: "event-system" }, "EventSystem already running");
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
log2.info(
|
|
1546
|
+
{ component: "event-system", eventName: StepEventNames.STEP_EXECUTION_RECEIVED },
|
|
1547
|
+
`Adding debug listener BEFORE subscriber for ${StepEventNames.STEP_EXECUTION_RECEIVED}...`
|
|
1548
|
+
);
|
|
1549
|
+
this.emitter.on(
|
|
1550
|
+
StepEventNames.STEP_EXECUTION_RECEIVED,
|
|
1551
|
+
(payload) => {
|
|
1552
|
+
log2.info(
|
|
1553
|
+
{
|
|
1554
|
+
component: "event-system",
|
|
1555
|
+
debugListener: true,
|
|
1556
|
+
eventId: payload.event?.event_id,
|
|
1557
|
+
stepUuid: payload.event?.step_uuid,
|
|
1558
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
1559
|
+
},
|
|
1560
|
+
`\u{1F50D} DEBUG LISTENER: Received ${StepEventNames.STEP_EXECUTION_RECEIVED} event!`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
log2.info(
|
|
1565
|
+
{
|
|
1566
|
+
component: "event-system",
|
|
1567
|
+
listenerCountAfterDebug: this.emitter.listenerCount(StepEventNames.STEP_EXECUTION_RECEIVED),
|
|
1568
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
1569
|
+
},
|
|
1570
|
+
"Debug listener added"
|
|
1571
|
+
);
|
|
1572
|
+
log2.info({ component: "event-system" }, "Starting subscriber first...");
|
|
1573
|
+
this.subscriber.start();
|
|
1574
|
+
log2.info(
|
|
1575
|
+
{
|
|
1576
|
+
component: "event-system",
|
|
1577
|
+
listenerCountAfterSubscriber: this.emitter.listenerCount(
|
|
1578
|
+
StepEventNames.STEP_EXECUTION_RECEIVED
|
|
1579
|
+
),
|
|
1580
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
1581
|
+
},
|
|
1582
|
+
"Subscriber started, checking listener count"
|
|
1583
|
+
);
|
|
1584
|
+
log2.info({ component: "event-system" }, "Starting poller...");
|
|
1585
|
+
this.poller.start();
|
|
1586
|
+
this.running = true;
|
|
1587
|
+
log2.info(
|
|
1588
|
+
{
|
|
1589
|
+
component: "event-system",
|
|
1590
|
+
emitterInstanceId: this.emitter.getInstanceId(),
|
|
1591
|
+
listenerCount: this.emitter.listenerCount(StepEventNames.STEP_EXECUTION_RECEIVED),
|
|
1592
|
+
eventName: StepEventNames.STEP_EXECUTION_RECEIVED
|
|
1593
|
+
},
|
|
1594
|
+
"EventSystem started successfully"
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Stop the event system gracefully.
|
|
1599
|
+
*
|
|
1600
|
+
* Stops ingress first (poller), waits for in-flight handlers,
|
|
1601
|
+
* then stops the subscriber.
|
|
1602
|
+
*
|
|
1603
|
+
* @param drainTimeoutMs - Maximum time to wait for in-flight handlers (default: 30000)
|
|
1604
|
+
*/
|
|
1605
|
+
async stop(drainTimeoutMs = 3e4) {
|
|
1606
|
+
if (!this.running) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
await this.poller.stop();
|
|
1610
|
+
await this.subscriber.waitForCompletion(drainTimeoutMs);
|
|
1611
|
+
this.subscriber.stop();
|
|
1612
|
+
this.running = false;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Check if the event system is running.
|
|
1616
|
+
*/
|
|
1617
|
+
isRunning() {
|
|
1618
|
+
return this.running;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Get the event emitter (for testing or advanced use cases).
|
|
1622
|
+
*/
|
|
1623
|
+
getEmitter() {
|
|
1624
|
+
return this.emitter;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Get current statistics about the event system.
|
|
1628
|
+
*/
|
|
1629
|
+
getStats() {
|
|
1630
|
+
return {
|
|
1631
|
+
running: this.running,
|
|
1632
|
+
processedCount: this.subscriber.getProcessedCount(),
|
|
1633
|
+
errorCount: this.subscriber.getErrorCount(),
|
|
1634
|
+
activeHandlers: this.subscriber.getActiveHandlers(),
|
|
1635
|
+
pollCount: this.poller.getPollCount()
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
export { EventNames, EventPoller, EventSystem, MetricsEventNames, PollerEventNames, StepEventNames, TaskerEventEmitter, WorkerEventNames, createEventPoller };
|
|
1641
|
+
//# sourceMappingURL=index.js.map
|
|
1642
|
+
//# sourceMappingURL=index.js.map
|