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