@supergrowthai/tq 1.0.13 → 1.1.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.
Files changed (38) hide show
  1. package/README.md +149 -8
  2. package/dist/{AsyncActions-CZYO8ShR.js → AsyncActions-B8ImDgTo.js} +39 -3
  3. package/dist/AsyncActions-B8ImDgTo.js.map +1 -0
  4. package/dist/{AsyncActions-BOO1ikWz.cjs → AsyncActions-BsxMX_Ib.cjs} +39 -3
  5. package/dist/AsyncActions-BsxMX_Ib.cjs.map +1 -0
  6. package/dist/core/Actions.cjs +23 -1
  7. package/dist/core/Actions.cjs.map +1 -1
  8. package/dist/core/Actions.mjs +23 -1
  9. package/dist/core/Actions.mjs.map +1 -1
  10. package/dist/core/async/AsyncActions.cjs +1 -1
  11. package/dist/core/async/AsyncActions.mjs +1 -1
  12. package/dist/index.cjs +459 -226
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.mjs +459 -226
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/src/core/Actions.d.cts +5 -1
  17. package/dist/src/core/Actions.d.ts +5 -1
  18. package/dist/src/core/TaskHandler.d.cts +6 -0
  19. package/dist/src/core/TaskHandler.d.ts +6 -0
  20. package/dist/src/core/TaskRunner.d.cts +22 -5
  21. package/dist/src/core/TaskRunner.d.ts +22 -5
  22. package/dist/src/core/async/AsyncActions.d.cts +1 -0
  23. package/dist/src/core/async/AsyncActions.d.ts +1 -0
  24. package/dist/src/core/flow/FlowMiddleware.d.cts +6 -1
  25. package/dist/src/core/flow/FlowMiddleware.d.ts +6 -1
  26. package/dist/src/core/flow/IFlowBarrierProvider.d.cts +4 -0
  27. package/dist/src/core/flow/IFlowBarrierProvider.d.ts +4 -0
  28. package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.cts +1 -0
  29. package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.ts +1 -0
  30. package/dist/src/core/lifecycle.d.cts +98 -3
  31. package/dist/src/core/lifecycle.d.ts +98 -3
  32. package/dist/src/providers/ConsoleHealthProvider.d.cts +42 -2
  33. package/dist/src/providers/ConsoleHealthProvider.d.ts +42 -2
  34. package/dist/src/test/lifecycle-events.test.d.cts +31 -0
  35. package/dist/src/test/lifecycle-events.test.d.ts +31 -0
  36. package/package.json +2 -2
  37. package/dist/AsyncActions-BOO1ikWz.cjs.map +0 -1
  38. package/dist/AsyncActions-CZYO8ShR.js.map +0 -1
package/README.md CHANGED
@@ -119,17 +119,25 @@ taskHandler.processMatureTasks();
119
119
 
120
120
  ### TaskRunner
121
121
 
122
- Handles task execution with locking and async support:
122
+ Handles task execution with locking and async support. Internal to TaskHandler — not typically instantiated directly.
123
123
 
124
124
  ```typescript
125
125
  import {TaskRunner} from '@supergrowthai/tq';
126
126
 
127
- const taskRunner = new TaskRunner(
127
+ const taskRunner = new TaskRunner({
128
128
  messageQueue,
129
129
  taskQueue,
130
130
  taskStore,
131
- cacheProvider
132
- );
131
+ cacheProvider,
132
+ generateId: databaseAdapter.generateId.bind(databaseAdapter),
133
+ lifecycleProvider, // optional
134
+ lifecycleConfig, // optional
135
+ entityProjection, // optional
136
+ entityProjectionConfig, // optional
137
+ flowMiddleware, // optional — typically created by TaskHandler
138
+ flowLifecycleProvider, // optional
139
+ workerId: 'my-worker', // optional
140
+ });
133
141
 
134
142
  // Run tasks
135
143
  const result = await taskRunner.run(
@@ -364,7 +372,7 @@ const workerLifecycleProvider: IWorkerLifecycleProvider = {
364
372
  console.log(`Heartbeat: ${info.worker_id}, processed: ${info.stats.tasks_processed}`);
365
373
  },
366
374
 
367
- onWorkerStopped(info: WorkerInfo & { reason: string; final_stats: WorkerStats }) {
375
+ onWorkerStopped(info: WorkerInfo & { reason: 'shutdown' | 'error' | 'idle_timeout'; final_stats: WorkerStats }) {
368
376
  console.log(`Worker stopped: ${info.worker_id}, reason: ${info.reason}`);
369
377
  },
370
378
 
@@ -374,6 +382,62 @@ const workerLifecycleProvider: IWorkerLifecycleProvider = {
374
382
 
375
383
  onBatchCompleted(info: WorkerInfo & { batch_size: number; succeeded: number; failed: number }) {
376
384
  console.log(`Batch completed: ${info.succeeded}/${info.batch_size} succeeded`);
385
+ },
386
+
387
+ onConsumerStarted(info: ConsumerInfo) {
388
+ console.log(`Consumer started: ${info.consumer_id} on queue ${info.queue_id}`);
389
+ },
390
+
391
+ onConsumerStopped(info: ConsumerInfo & { reason: 'shutdown' | 'error' | 'idle_timeout'; stats: ConsumerStats }) {
392
+ console.log(`Consumer stopped: ${info.consumer_id}, processed: ${info.stats.tasks_processed}`);
393
+ }
394
+ };
395
+ ```
396
+
397
+ The heartbeat callback also includes `active_consumers: ConsumerStats[]` for per-consumer-per-worker visibility in SRE dashboards.
398
+
399
+ ### Flow Lifecycle Provider
400
+
401
+ Track flow orchestration events (fan-out/fan-in):
402
+
403
+ ```typescript
404
+ import {IFlowLifecycleProvider, FlowContext} from '@supergrowthai/tq';
405
+
406
+ const flowLifecycleProvider: IFlowLifecycleProvider = {
407
+ onFlowStarted(ctx: FlowContext & { started_at: Date; step_types: string[] }) {
408
+ console.log(`Flow started: ${ctx.flow_id}, ${ctx.total_steps} steps`);
409
+ },
410
+
411
+ onFlowCompleted(ctx: FlowContext & { duration_ms: number; steps_succeeded: number; steps_failed: number }) {
412
+ console.log(`Flow completed: ${ctx.flow_id} in ${ctx.duration_ms}ms`);
413
+ },
414
+
415
+ onFlowAborted(ctx: FlowContext & { duration_ms: number; steps_completed: number; trigger_step_index: number }) {
416
+ console.log(`Flow aborted: ${ctx.flow_id}, trigger step: ${ctx.trigger_step_index}`);
417
+ },
418
+
419
+ onFlowTimedOut(ctx: FlowContext & { duration_ms: number; steps_completed: number }) {
420
+ console.log(`Flow timed out: ${ctx.flow_id}, ${ctx.steps_completed} steps completed`);
421
+ }
422
+ };
423
+ ```
424
+
425
+ Flow events are split across two pipeline stages:
426
+ - `onFlowStarted` fires during task execution (when `actions.startFlow()` is called)
427
+ - `onFlowCompleted/Aborted/TimedOut` fire during post-processing (when barriers resolve)
428
+
429
+ ### Batch Executor Lifecycle
430
+
431
+ Track multi-task (batch) executor events for task-level accounting:
432
+
433
+ ```typescript
434
+ const taskLifecycleProvider: ITaskLifecycleProvider = {
435
+ onTaskBatchStarted(ctx) {
436
+ console.log(`Batch started: ${ctx.task_type}, ${ctx.tasks.length} tasks`);
437
+ },
438
+
439
+ onTaskBatchCompleted(ctx) {
440
+ console.log(`Batch done: ${ctx.succeeded.length} ok, ${ctx.failed.length} failed in ${ctx.duration_ms}ms`);
377
441
  }
378
442
  };
379
443
  ```
@@ -437,6 +501,8 @@ const taskHandler = new TaskHandler(
437
501
  | max_retries | number | Maximum retry attempts |
438
502
  | scheduled_at | Date | When task was scheduled |
439
503
  | worker_id | string? | ID of worker processing the task |
504
+ | consumer_id | string? | Consumer stream identity (distinguishes multiple consumers on same worker) |
505
+ | log_context | Record<string, string>? | User-provided log correlation context (RFC-005) |
440
506
 
441
507
  ### WorkerInfo Properties
442
508
 
@@ -658,10 +724,9 @@ class RedisFlowBarrierProvider implements IFlowBarrierProvider {
658
724
  ### Wiring It Up
659
725
 
660
726
  ```typescript
661
- import { FlowMiddleware, InMemoryFlowBarrierProvider } from '@supergrowthai/tq';
727
+ import { InMemoryFlowBarrierProvider } from '@supergrowthai/tq';
662
728
 
663
729
  const barrierProvider = new InMemoryFlowBarrierProvider(); // or your Redis impl
664
- const flowMiddleware = new FlowMiddleware(barrierProvider, generateId);
665
730
 
666
731
  const taskHandler = new TaskHandler(
667
732
  messageQueue,
@@ -674,11 +739,14 @@ const taskHandler = new TaskHandler(
674
739
  lifecycleProvider: myLifecycleProvider,
675
740
  workerProvider: myWorkerProvider,
676
741
  entityProjection: myProjectionProvider,
677
- flowMiddleware, // RFC-002
742
+ flowBarrierProvider: barrierProvider, // RFC-002: TaskHandler creates FlowMiddleware internally
743
+ flowLifecycleProvider: myFlowLifecycleProvider, // optional: flow lifecycle events
678
744
  }
679
745
  );
680
746
  ```
681
747
 
748
+ TaskHandler assembles `FlowMiddleware` internally from the barrier provider — no need to construct it yourself. If `flowLifecycleProvider` is set without `flowBarrierProvider`, TaskHandler throws immediately (fail-fast).
749
+
682
750
  ### Entity Tracking on Flows
683
751
 
684
752
  When `config.entity` is provided, the flow lifecycle is projected through the same entity projection system (RFC-003):
@@ -700,6 +768,79 @@ Individual step tasks do **not** carry `CronTask.entity` — entity tracking is
700
768
  - **Nested flows**: A join executor can call `actions.startFlow()` to start another flow. Flow IDs are independent UUIDs — no special handling needed.
701
769
  - **IMultiTaskExecutor optimization**: Flow steps of the same type in the same processing cycle batch into a single executor call automatically via `TaskRunner`'s existing grouping logic.
702
770
 
771
+ ## API Reference — Lifecycle Interfaces
772
+
773
+ ### ITaskLifecycleProvider
774
+
775
+ | Method | Context | Description |
776
+ |--------|---------|-------------|
777
+ | `onTaskScheduled?(ctx)` | `TaskContext` | Task added to queue |
778
+ | `onTaskStarted?(ctx)` | `TaskContext & { started_at, queued_duration_ms }` | Worker picks up task |
779
+ | `onTaskCompleted?(ctx)` | `TaskContext & { timing: TaskTiming, result? }` | Task succeeds |
780
+ | `onTaskFailed?(ctx)` | `TaskContext & { timing: TaskTiming, error, will_retry, next_attempt_at? }` | Task fails (before retry decision) |
781
+ | `onTaskExhausted?(ctx)` | `TaskContext & { timing: TaskTiming, error, total_attempts }` | All retries exhausted |
782
+ | `onTaskCancelled?(ctx)` | `TaskContext & { reason }` | Task manually cancelled |
783
+ | `onTaskBatchStarted?(ctx)` | `{ task_type, queue_id, tasks: TaskContext[], worker_id, consumer_id?, started_at }` | Batch executor starts |
784
+ | `onTaskBatchCompleted?(ctx)` | `{ task_type, queue_id, tasks, worker_id, consumer_id?, succeeded: string[], failed: string[], duration_ms }` | Batch executor finishes |
785
+
786
+ ### IWorkerLifecycleProvider
787
+
788
+ | Method | Context | Description |
789
+ |--------|---------|-------------|
790
+ | `onWorkerStarted?(info)` | `WorkerInfo` | Worker process starts consuming |
791
+ | `onWorkerHeartbeat?(info)` | `WorkerInfo & { stats: WorkerStats, memory_usage_mb, active_consumers: ConsumerStats[] }` | Periodic health check |
792
+ | `onWorkerStopped?(info)` | `WorkerInfo & { reason, final_stats: WorkerStats }` | Worker shuts down |
793
+ | `onBatchStarted?(info)` | `WorkerInfo & { batch_size, task_types: string[] }` | Processing batch begins |
794
+ | `onBatchCompleted?(info)` | `WorkerInfo & { batch_size, succeeded, failed, duration_ms }` | Processing batch ends |
795
+ | `onConsumerStarted?(info)` | `ConsumerInfo` | First batch arrives on a consumer (lazy registration) |
796
+ | `onConsumerStopped?(info)` | `ConsumerInfo & { reason, stats: ConsumerStats }` | Consumer stops (shutdown) |
797
+
798
+ ### IFlowLifecycleProvider
799
+
800
+ | Method | Context | Description |
801
+ |--------|---------|-------------|
802
+ | `onFlowStarted?(ctx)` | `FlowContext & { started_at, step_types: string[] }` | `actions.startFlow()` called |
803
+ | `onFlowCompleted?(ctx)` | `FlowContext & { duration_ms, steps_succeeded, steps_failed }` | All steps done, barrier met |
804
+ | `onFlowAborted?(ctx)` | `FlowContext & { duration_ms, steps_completed, trigger_step_index }` | Step failed with `abort` policy |
805
+ | `onFlowTimedOut?(ctx)` | `FlowContext & { duration_ms, steps_completed }` | Timeout sentinel fired before barrier met |
806
+
807
+ ### IFlowBarrierProvider
808
+
809
+ | Method | Signature | Description |
810
+ |--------|-----------|-------------|
811
+ | `initBarrier` | `(flowId: string, totalSteps: number) => Promise<void>` | Initialize barrier for a new flow |
812
+ | `batchDecrementAndCheck` | `(flowId: string, results: FlowStepResult[]) => Promise<BarrierDecrementResult>` | Record step results, decrement barrier (HSETNX dedup) |
813
+ | `getStepResults` | `(flowId: string) => Promise<FlowStepResult[]>` | Get all recorded step results |
814
+ | `markAborted` | `(flowId: string) => Promise<boolean>` | Mark flow aborted (returns true on first call) |
815
+ | `isComplete` | `(flowId: string) => Promise<boolean>` | Check if barrier fully met |
816
+ | `getStartedAt` | `(flowId: string) => Promise<Date \| null>` | Get barrier init timestamp (for duration tracking) |
817
+
818
+ ### TaskHandlerConfig
819
+
820
+ | Field | Type | Description |
821
+ |-------|------|-------------|
822
+ | `lifecycleProvider?` | `ITaskLifecycleProvider` | Task lifecycle event callbacks |
823
+ | `workerProvider?` | `IWorkerLifecycleProvider` | Worker/consumer lifecycle event callbacks |
824
+ | `lifecycle?` | `TaskHandlerLifecycleConfig` | Callback mode (`sync`/`async`), heartbeat interval, payload inclusion |
825
+ | `entityProjection?` | `IEntityProjectionProvider` | Entity-task projection provider (RFC-003) |
826
+ | `entityProjectionConfig?` | `EntityProjectionConfig` | Projection options (`includePayload`) |
827
+ | `flowBarrierProvider?` | `IFlowBarrierProvider` | Flow barrier provider — enables flow orchestration (RFC-002) |
828
+ | `flowLifecycleProvider?` | `IFlowLifecycleProvider` | Flow lifecycle events (requires `flowBarrierProvider`) |
829
+
830
+ ### Supporting Types
831
+
832
+ | Type | Fields | Description |
833
+ |------|--------|-------------|
834
+ | `TaskContext` | `task_id, task_hash?, task_type, queue_id, payload, attempt, max_retries, scheduled_at, worker_id?, consumer_id?, log_context?` | Core task identity passed to all lifecycle callbacks |
835
+ | `TaskTiming` | `queued_duration_ms, processing_duration_ms, total_duration_ms` | Timing breakdown for completed/failed tasks |
836
+ | `WorkerInfo` | `worker_id, hostname, pid, started_at, enabled_queues` | Worker process identity |
837
+ | `WorkerStats` | `tasks_processed, tasks_succeeded, tasks_failed, avg_processing_ms, current_task?` | Aggregate worker metrics |
838
+ | `ConsumerInfo` | `consumer_id, queue_id, worker_id, started_at` | Consumer stream identity |
839
+ | `ConsumerStats` | `consumer_id, queue_id, tasks_processed, tasks_succeeded, tasks_failed, last_task_at?` | Per-consumer metrics |
840
+ | `FlowContext` | `flow_id, total_steps, join, failure_policy, entity?, worker_id, consumer_id?` | Flow identity for lifecycle events |
841
+ | `FlowStepResult` | `step_index, status, result?, error?` | Individual step outcome in barrier |
842
+ | `BarrierDecrementResult` | `remaining` | `0` = barrier met, `>0` = pending, `-1` = already complete/aborted |
843
+
703
844
  ## Error Handling and Retries
704
845
 
705
846
  ```typescript
@@ -207,8 +207,17 @@ class AsyncActions {
207
207
  const executor = this.taskQueue.getExecutor(task.queue_id, task.type);
208
208
  const shouldStoreOnFailure = executor?.store_on_failure ?? false;
209
209
  const id = shouldStoreOnFailure ? { id: this.generateId() } : {};
210
- return { ...id, ...task };
210
+ const partitionKey = executor?.getPartitionKey?.(task);
211
+ return { ...id, ...task, ...partitionKey ? { partition_key: partitionKey } : {} };
211
212
  });
213
+ if (this.entityProjection) {
214
+ try {
215
+ const projections = queueTasks.filter((t) => t.entity).map((t) => buildProjection(t, "scheduled", { includePayload: this.entityProjectionConfig?.includePayload })).filter((p) => p !== null);
216
+ await syncProjections(projections, this.entityProjection, logger);
217
+ } catch (err) {
218
+ logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);
219
+ }
220
+ }
212
221
  try {
213
222
  await this.messageQueue.addMessages(queue, queueTasks);
214
223
  logger.info(`[AsyncActions] Added ${queueTasks.length} immediate tasks to queue ${queue}`);
@@ -216,14 +225,32 @@ class AsyncActions {
216
225
  logger.error(`[AsyncActions] Failed to add tasks to queue ${queue}:`, err);
217
226
  throw err;
218
227
  }
228
+ if (this.lifecycleEmitter?.onScheduled) {
229
+ for (const task of queueTasks) {
230
+ try {
231
+ this.lifecycleEmitter.onScheduled(task);
232
+ } catch (err) {
233
+ logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);
234
+ }
235
+ }
236
+ }
219
237
  }
220
238
  if (future.length > 0) {
221
239
  const futureTasks = future.map((task) => {
222
240
  const executor = this.taskQueue.getExecutor(task.queue_id, task.type);
223
241
  const shouldStoreOnFailure = executor?.store_on_failure ?? false;
224
242
  const id = shouldStoreOnFailure ? { id: this.generateId() } : {};
225
- return { ...id, ...task };
243
+ const partitionKey = executor?.getPartitionKey?.(task);
244
+ return { ...id, ...task, ...partitionKey ? { partition_key: partitionKey } : {} };
226
245
  });
246
+ if (this.entityProjection) {
247
+ try {
248
+ const projections = futureTasks.filter((t) => t.entity).map((t) => buildProjection(t, "scheduled", { includePayload: this.entityProjectionConfig?.includePayload })).filter((p) => p !== null);
249
+ await syncProjections(projections, this.entityProjection, logger);
250
+ } catch (err) {
251
+ logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);
252
+ }
253
+ }
227
254
  try {
228
255
  await this.taskStore.addTasksToScheduled(futureTasks);
229
256
  logger.info(`[AsyncActions] Added ${futureTasks.length} future tasks to database`);
@@ -231,6 +258,15 @@ class AsyncActions {
231
258
  logger.error(`[AsyncActions] Failed to add tasks to database:`, err);
232
259
  throw err;
233
260
  }
261
+ if (this.lifecycleEmitter?.onScheduled) {
262
+ for (const task of futureTasks) {
263
+ try {
264
+ this.lifecycleEmitter.onScheduled(task);
265
+ } catch (err) {
266
+ logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);
267
+ }
268
+ }
269
+ }
234
270
  }
235
271
  }
236
272
  }
@@ -239,4 +275,4 @@ export {
239
275
  buildProjection as b,
240
276
  syncProjections as s
241
277
  };
242
- //# sourceMappingURL=AsyncActions-CZYO8ShR.js.map
278
+ //# sourceMappingURL=AsyncActions-B8ImDgTo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AsyncActions-B8ImDgTo.js","sources":["../src/core/async/retry-utils.ts","../src/core/entity/IEntityProjectionProvider.ts","../src/core/async/AsyncActions.ts"],"sourcesContent":["import {CronTask} from \"../../adapters\";\n\nconst MAX_RETRY_DELAY_MS = 5 * 60 * 1000; // 5 minutes\n\nexport interface RetryDecision<ID = any> {\n action: 'retry' | 'fail';\n retryTask?: CronTask<ID>;\n}\n\n/**\n * Compute whether a failed task should be retried and produce the retry-ready task.\n *\n * Mirrors the logic in TaskHandler.postProcessTasks() but usable from the async path.\n *\n * @param task The failed task\n * @param maxRetries Maximum retry attempts for this task type\n * @returns RetryDecision — either { action: 'retry', retryTask } or { action: 'fail' }\n */\nexport function computeRetryDecision<ID>(\n task: CronTask<ID>,\n maxRetries: number\n): RetryDecision<ID> {\n const retryCount = (task.execution_stats && typeof task.execution_stats.retry_count === 'number')\n ? task.execution_stats.retry_count\n : 0;\n\n if (retryCount >= maxRetries) {\n return {action: 'fail'};\n }\n\n const taskRetryAfter = Math.max(task.retry_after || 2000, 0);\n const calculatedDelay = taskRetryAfter * Math.pow(retryCount + 1, 2);\n const retryAfter = Math.min(calculatedDelay, MAX_RETRY_DELAY_MS);\n const executeAt = Date.now() + retryAfter;\n\n return {\n action: 'retry',\n retryTask: {\n ...task,\n status: 'scheduled' as const,\n execute_at: new Date(executeAt),\n execution_stats: {\n ...(task.execution_stats || {}),\n retry_count: retryCount + 1\n }\n }\n };\n}\n","/**\n * RFC-003: Entity Task Projection\n *\n * Automatic entity-task status projection so dashboards can track\n * task lifecycle without querying the internal tasks table.\n */\n\nimport type {CronTask} from \"../../adapters/types.js\";\n\nexport type EntityTaskProjectionStatus = 'scheduled' | 'processing' | 'executed' | 'failed';\n\nexport interface EntityTaskProjection<ID = any> {\n task_id: ID;\n entity_id: string;\n entity_type: string;\n task_type: string;\n queue_id: string;\n status: EntityTaskProjectionStatus;\n payload?: unknown;\n error?: string;\n result?: unknown;\n created_at: Date;\n updated_at: Date;\n}\n\n/**\n * Provider interface for persisting entity-task projections.\n * Implementations might write to a database table, cache, or external service.\n */\nexport interface IEntityProjectionProvider<ID = any> {\n upsertProjections(entries: EntityTaskProjection<ID>[]): Promise<void>;\n}\n\n/**\n * Configuration for entity projection behavior.\n */\nexport interface EntityProjectionConfig {\n /** Include task payload in projection (default: false for performance) */\n includePayload?: boolean;\n}\n\n/**\n * Sync entity projections to the provider. Non-fatal — logs and continues on error.\n */\nexport async function syncProjections<ID>(\n projections: EntityTaskProjection<ID>[],\n provider: IEntityProjectionProvider<ID> | undefined,\n logger: { error(msg: string): void }\n): Promise<void> {\n if (projections.length === 0 || !provider) return;\n try {\n await provider.upsertProjections(projections);\n } catch (err) {\n logger.error(`[TQ] Entity projection sync failed (non-fatal): ${err}`);\n }\n}\n\n/**\n * Build a single projection entry from a CronTask and target status.\n * Returns null if the task has no entity binding.\n * Throws if entity is present but task has no ID — fail-fast for developer errors.\n */\nexport function buildProjection<ID>(\n task: CronTask<ID>,\n status: EntityTaskProjectionStatus,\n options?: {\n includePayload?: boolean;\n error?: string;\n result?: unknown;\n }\n): EntityTaskProjection<ID> | null {\n if (!task.entity) return null;\n\n if (task.id == null) {\n throw new Error(\n `[TQ/RFC-003] Task with entity (${task.entity.type}:${task.entity.id}) has no task ID. ` +\n `Entity-bearing tasks must have an ID for projection keying. ` +\n `Set store_on_failure:true, force_store:true, or assign an ID before addTasks().`\n );\n }\n\n return {\n task_id: task.id,\n entity_id: task.entity.id,\n entity_type: task.entity.type,\n task_type: task.type,\n queue_id: task.queue_id,\n status,\n payload: options?.includePayload ? task.payload : undefined,\n error: options?.error,\n result: options?.result,\n created_at: task.created_at || new Date(),\n updated_at: new Date(),\n };\n}","import {Logger, LogLevel} from \"@supergrowthai/utils\";\nimport {TaskStore} from \"../TaskStore.js\";\nimport {Actions} from \"../Actions.js\";\nimport {IMessageQueue, QueueName} from \"@supergrowthai/mq\";\nimport {tId} from \"../../utils/task-id-gen.js\";\nimport {CronTask} from \"../../adapters\";\nimport {TaskQueuesManager} from \"../TaskQueuesManager.js\";\nimport {computeRetryDecision} from \"./retry-utils.js\";\nimport type {IEntityProjectionProvider, EntityProjectionConfig, EntityTaskProjection} from \"../entity/IEntityProjectionProvider.js\";\nimport {buildProjection, syncProjections} from \"../entity/IEntityProjectionProvider.js\";\nimport type {FlowMiddleware} from \"../flow/FlowMiddleware.js\";\n\nconst logger = new Logger('AsyncActions', LogLevel.INFO);\n\n/**\n * Interface for emitting async task lifecycle events.\n * Constructed from the sync-path's ITaskLifecycleProvider by TaskRunner.\n */\nexport interface AsyncLifecycleEmitter {\n onCompleted(task: CronTask<any>, result?: unknown): void;\n onFailed(task: CronTask<any>, error: Error, willRetry: boolean): void;\n onScheduled?(task: CronTask<any>): void;\n}\n\nexport class AsyncActions<ID = any> {\n private readonly actions: Actions<ID>;\n private readonly taskId: string;\n\n constructor(\n private messageQueue: IMessageQueue<ID>,\n private taskStore: TaskStore<ID>,\n private taskQueue: TaskQueuesManager<ID>,\n actions: Actions<ID>,\n private task: CronTask<ID>,\n private generateId: () => ID,\n private lifecycleEmitter?: AsyncLifecycleEmitter,\n private entityProjection?: IEntityProjectionProvider<ID>,\n private entityProjectionConfig?: EntityProjectionConfig,\n private flowMiddleware?: FlowMiddleware<ID>\n ) {\n this.actions = actions;\n this.taskId = tId(task);\n }\n\n /**\n * Called when the async promise completes to execute the collected actions\n */\n async onPromiseFulfilled(): Promise<void> {\n // Extract this task's results (NO batch context for async tasks)\n const results = this.actions.extractTaskActions(this.taskId);\n\n // If task didn't call success or fail, default to fail (forgetful executor)\n const hasCompletion = results.successTasks.length > 0 || results.failedTasks.length > 0;\n if (!hasCompletion) {\n logger.warn(`Async task ${this.taskId} completed without calling success() or fail() — defaulting to fail`);\n results.failedTasks.push({\n ...this.task,\n execution_stats: {\n ...(this.task.execution_stats || {}),\n last_error: `Async task ${this.taskId} completed without calling success() or fail()`,\n }\n });\n }\n\n logger.info(`[AsyncActions] Processing results for async task ${this.taskId}: ` +\n `${results.successTasks.length} success, ${results.failedTasks.length} failed, ` +\n `${results.newTasks.length} new tasks`);\n\n // Process failed tasks with retry logic\n if (results.failedTasks.length > 0) {\n for (const failedTask of results.failedTasks) {\n try {\n await this.processFailedTaskWithRetry(failedTask);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to process failed task:`, err);\n throw err;\n }\n }\n }\n\n if (results.successTasks.length > 0) {\n try {\n await this.taskStore.markTasksAsSuccess(results.successTasks);\n logger.info(`[AsyncActions] Marked ${results.successTasks.length} tasks as success in database`);\n\n // Emit lifecycle event for each success\n if (this.lifecycleEmitter) {\n for (const task of results.successTasks) {\n try {\n this.lifecycleEmitter.onCompleted(task, task.execution_result);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onCompleted error:`, err);\n }\n }\n }\n\n // RFC-003: Emit 'executed' entity projections for async success tasks\n await syncProjections(\n results.successTasks\n .map(t => buildProjection(t, 'executed', {\n includePayload: this.entityProjectionConfig?.includePayload,\n result: t.execution_result,\n }))\n .filter((p): p is EntityTaskProjection<ID> => p !== null),\n this.entityProjection,\n logger\n );\n } catch (err) {\n logger.error(`[AsyncActions] Failed to mark tasks as success:`, err);\n throw err;\n }\n }\n\n // RFC-002: Flow middleware — process terminal tasks for barrier tracking and join dispatch\n if (this.flowMiddleware) {\n try {\n // Collect final failures (not retries) for flow middleware\n const finalFailedTasks: CronTask<ID>[] = [];\n for (const failedTask of results.failedTasks) {\n const executor = this.taskQueue.getExecutor(failedTask.queue_id, failedTask.type);\n const maxRetries = failedTask.retries ?? executor?.default_retries ?? 0;\n const decision = computeRetryDecision(failedTask, maxRetries);\n // Only include final failures (no more retries)\n if (decision.action !== 'retry' || !decision.retryTask) {\n finalFailedTasks.push(failedTask);\n }\n }\n\n if (results.successTasks.length > 0 || finalFailedTasks.length > 0) {\n const flowResult = await this.flowMiddleware.onPostProcess({\n successTasks: results.successTasks,\n failedTasks: finalFailedTasks,\n });\n\n if (flowResult.projections.length > 0 && this.entityProjection) {\n await syncProjections(flowResult.projections, this.entityProjection, logger);\n }\n\n if (flowResult.joinTasks.length > 0) {\n await this.scheduleNewTasks(flowResult.joinTasks);\n }\n }\n } catch (err) {\n logger.error(`[AsyncActions] Flow middleware failed (non-fatal): ${err}`);\n }\n }\n\n if (results.newTasks.length > 0) {\n logger.info(`[AsyncActions] Scheduling ${results.newTasks.length} new tasks`);\n await this.scheduleNewTasks(results.newTasks);\n }\n }\n\n /**\n * Process a failed task through the retry pipeline\n */\n private async processFailedTaskWithRetry(failedTask: CronTask<ID>): Promise<void> {\n const executor = this.taskQueue.getExecutor(failedTask.queue_id, failedTask.type);\n const maxRetries = failedTask.retries ?? executor?.default_retries ?? 0;\n const decision = computeRetryDecision(failedTask, maxRetries);\n const willRetry = decision.action === 'retry' && !!decision.retryTask;\n\n if (willRetry) {\n logger.info(`[AsyncActions] Retrying async task ${this.taskId} (attempt ${(decision.retryTask!.execution_stats?.retry_count as number) || 0})`);\n await this.taskStore.updateTasksForRetry([decision.retryTask!]);\n } else {\n logger.info(`[AsyncActions] Async task ${this.taskId} exhausted retries, marking as failed`);\n await this.taskStore.markTasksAsFailed([failedTask]);\n\n // RFC-003: Emit 'failed' entity projection for final-failed async tasks\n const errorMsg = failedTask.execution_stats?.last_error as string || 'Task failed';\n const p = buildProjection(failedTask, 'failed', {\n includePayload: this.entityProjectionConfig?.includePayload,\n error: errorMsg,\n });\n if (p) await syncProjections([p], this.entityProjection, logger);\n }\n\n if (this.lifecycleEmitter) {\n const errorMsg = failedTask.execution_stats?.last_error as string || 'Task failed';\n try {\n this.lifecycleEmitter.onFailed(failedTask, new Error(errorMsg), willRetry);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onFailed error:`, err);\n }\n }\n }\n\n /**\n * Schedule new tasks - replicates the logic from task-handler's addTasks\n */\n private async scheduleNewTasks(tasks: CronTask<ID>[]): Promise<void> {\n const now = new Date();\n const immediate: { [key in QueueName]?: CronTask<ID>[] } = {};\n const future: CronTask<ID>[] = [];\n\n // Split tasks by timing\n for (const task of tasks) {\n const timeDiff = (task.execute_at.getTime() - now.getTime()) / 1000 / 60; // in minutes\n\n if (timeDiff > 2) {\n // Future task - goes to database\n future.push(task);\n } else {\n // Immediate task - goes to message queue\n const queue = task.queue_id;\n if (!immediate[queue]) {\n immediate[queue] = [];\n }\n immediate[queue].push(task);\n }\n }\n\n // Process immediate tasks\n const iQueues = Object.keys(immediate) as QueueName[];\n for (const queue of iQueues) {\n const queueTasks = immediate[queue]!.map((task) => {\n const executor = this.taskQueue.getExecutor(task.queue_id, task.type);\n const shouldStoreOnFailure = executor?.store_on_failure ?? false;\n const id = shouldStoreOnFailure ? {id: this.generateId()} : {};\n const partitionKey = executor?.getPartitionKey?.(task);\n return {...id, ...task, ...(partitionKey ? {partition_key: partitionKey} : {})};\n });\n\n // Entity projections for scheduled tasks (mirrors TaskHandler.addTasks pattern)\n if (this.entityProjection) {\n try {\n const projections = queueTasks\n .filter(t => t.entity)\n .map(t => buildProjection(t, 'scheduled', {includePayload: this.entityProjectionConfig?.includePayload}))\n .filter((p): p is EntityTaskProjection<ID> => p !== null);\n await syncProjections(projections, this.entityProjection, logger);\n } catch (err) {\n logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);\n }\n }\n\n try {\n await this.messageQueue.addMessages(queue, queueTasks as CronTask<ID>[]);\n logger.info(`[AsyncActions] Added ${queueTasks.length} immediate tasks to queue ${queue}`);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to add tasks to queue ${queue}:`, err);\n throw err;\n }\n\n // Emit onScheduled lifecycle event for each task\n if (this.lifecycleEmitter?.onScheduled) {\n for (const task of queueTasks) {\n try {\n this.lifecycleEmitter.onScheduled(task);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);\n }\n }\n }\n }\n\n // Process future tasks\n if (future.length > 0) {\n const futureTasks = future.map((task) => {\n const executor = this.taskQueue.getExecutor(task.queue_id, task.type);\n const shouldStoreOnFailure = executor?.store_on_failure ?? false;\n const id = shouldStoreOnFailure ? {id: this.generateId()} : {};\n const partitionKey = executor?.getPartitionKey?.(task);\n return {...id, ...task, ...(partitionKey ? {partition_key: partitionKey} : {})};\n });\n\n // Entity projections for future scheduled tasks\n if (this.entityProjection) {\n try {\n const projections = futureTasks\n .filter(t => t.entity)\n .map(t => buildProjection(t, 'scheduled', {includePayload: this.entityProjectionConfig?.includePayload}))\n .filter((p): p is EntityTaskProjection<ID> => p !== null);\n await syncProjections(projections, this.entityProjection, logger);\n } catch (err) {\n logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);\n }\n }\n\n try {\n await this.taskStore.addTasksToScheduled(futureTasks);\n logger.info(`[AsyncActions] Added ${futureTasks.length} future tasks to database`);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to add tasks to database:`, err);\n throw err;\n }\n\n // Emit onScheduled lifecycle event for future tasks\n if (this.lifecycleEmitter?.onScheduled) {\n for (const task of futureTasks) {\n try {\n this.lifecycleEmitter.onScheduled(task);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);\n }\n }\n }\n }\n }\n}\n"],"names":["logger"],"mappings":"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"}
@@ -208,8 +208,17 @@ class AsyncActions {
208
208
  const executor = this.taskQueue.getExecutor(task.queue_id, task.type);
209
209
  const shouldStoreOnFailure = executor?.store_on_failure ?? false;
210
210
  const id = shouldStoreOnFailure ? { id: this.generateId() } : {};
211
- return { ...id, ...task };
211
+ const partitionKey = executor?.getPartitionKey?.(task);
212
+ return { ...id, ...task, ...partitionKey ? { partition_key: partitionKey } : {} };
212
213
  });
214
+ if (this.entityProjection) {
215
+ try {
216
+ const projections = queueTasks.filter((t) => t.entity).map((t) => buildProjection(t, "scheduled", { includePayload: this.entityProjectionConfig?.includePayload })).filter((p) => p !== null);
217
+ await syncProjections(projections, this.entityProjection, logger);
218
+ } catch (err) {
219
+ logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);
220
+ }
221
+ }
213
222
  try {
214
223
  await this.messageQueue.addMessages(queue, queueTasks);
215
224
  logger.info(`[AsyncActions] Added ${queueTasks.length} immediate tasks to queue ${queue}`);
@@ -217,14 +226,32 @@ class AsyncActions {
217
226
  logger.error(`[AsyncActions] Failed to add tasks to queue ${queue}:`, err);
218
227
  throw err;
219
228
  }
229
+ if (this.lifecycleEmitter?.onScheduled) {
230
+ for (const task of queueTasks) {
231
+ try {
232
+ this.lifecycleEmitter.onScheduled(task);
233
+ } catch (err) {
234
+ logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);
235
+ }
236
+ }
237
+ }
220
238
  }
221
239
  if (future.length > 0) {
222
240
  const futureTasks = future.map((task) => {
223
241
  const executor = this.taskQueue.getExecutor(task.queue_id, task.type);
224
242
  const shouldStoreOnFailure = executor?.store_on_failure ?? false;
225
243
  const id = shouldStoreOnFailure ? { id: this.generateId() } : {};
226
- return { ...id, ...task };
244
+ const partitionKey = executor?.getPartitionKey?.(task);
245
+ return { ...id, ...task, ...partitionKey ? { partition_key: partitionKey } : {} };
227
246
  });
247
+ if (this.entityProjection) {
248
+ try {
249
+ const projections = futureTasks.filter((t) => t.entity).map((t) => buildProjection(t, "scheduled", { includePayload: this.entityProjectionConfig?.includePayload })).filter((p) => p !== null);
250
+ await syncProjections(projections, this.entityProjection, logger);
251
+ } catch (err) {
252
+ logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);
253
+ }
254
+ }
228
255
  try {
229
256
  await this.taskStore.addTasksToScheduled(futureTasks);
230
257
  logger.info(`[AsyncActions] Added ${futureTasks.length} future tasks to database`);
@@ -232,10 +259,19 @@ class AsyncActions {
232
259
  logger.error(`[AsyncActions] Failed to add tasks to database:`, err);
233
260
  throw err;
234
261
  }
262
+ if (this.lifecycleEmitter?.onScheduled) {
263
+ for (const task of futureTasks) {
264
+ try {
265
+ this.lifecycleEmitter.onScheduled(task);
266
+ } catch (err) {
267
+ logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);
268
+ }
269
+ }
270
+ }
235
271
  }
236
272
  }
237
273
  }
238
274
  exports.AsyncActions = AsyncActions;
239
275
  exports.buildProjection = buildProjection;
240
276
  exports.syncProjections = syncProjections;
241
- //# sourceMappingURL=AsyncActions-BOO1ikWz.cjs.map
277
+ //# sourceMappingURL=AsyncActions-BsxMX_Ib.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AsyncActions-BsxMX_Ib.cjs","sources":["../src/core/async/retry-utils.ts","../src/core/entity/IEntityProjectionProvider.ts","../src/core/async/AsyncActions.ts"],"sourcesContent":["import {CronTask} from \"../../adapters\";\n\nconst MAX_RETRY_DELAY_MS = 5 * 60 * 1000; // 5 minutes\n\nexport interface RetryDecision<ID = any> {\n action: 'retry' | 'fail';\n retryTask?: CronTask<ID>;\n}\n\n/**\n * Compute whether a failed task should be retried and produce the retry-ready task.\n *\n * Mirrors the logic in TaskHandler.postProcessTasks() but usable from the async path.\n *\n * @param task The failed task\n * @param maxRetries Maximum retry attempts for this task type\n * @returns RetryDecision — either { action: 'retry', retryTask } or { action: 'fail' }\n */\nexport function computeRetryDecision<ID>(\n task: CronTask<ID>,\n maxRetries: number\n): RetryDecision<ID> {\n const retryCount = (task.execution_stats && typeof task.execution_stats.retry_count === 'number')\n ? task.execution_stats.retry_count\n : 0;\n\n if (retryCount >= maxRetries) {\n return {action: 'fail'};\n }\n\n const taskRetryAfter = Math.max(task.retry_after || 2000, 0);\n const calculatedDelay = taskRetryAfter * Math.pow(retryCount + 1, 2);\n const retryAfter = Math.min(calculatedDelay, MAX_RETRY_DELAY_MS);\n const executeAt = Date.now() + retryAfter;\n\n return {\n action: 'retry',\n retryTask: {\n ...task,\n status: 'scheduled' as const,\n execute_at: new Date(executeAt),\n execution_stats: {\n ...(task.execution_stats || {}),\n retry_count: retryCount + 1\n }\n }\n };\n}\n","/**\n * RFC-003: Entity Task Projection\n *\n * Automatic entity-task status projection so dashboards can track\n * task lifecycle without querying the internal tasks table.\n */\n\nimport type {CronTask} from \"../../adapters/types.js\";\n\nexport type EntityTaskProjectionStatus = 'scheduled' | 'processing' | 'executed' | 'failed';\n\nexport interface EntityTaskProjection<ID = any> {\n task_id: ID;\n entity_id: string;\n entity_type: string;\n task_type: string;\n queue_id: string;\n status: EntityTaskProjectionStatus;\n payload?: unknown;\n error?: string;\n result?: unknown;\n created_at: Date;\n updated_at: Date;\n}\n\n/**\n * Provider interface for persisting entity-task projections.\n * Implementations might write to a database table, cache, or external service.\n */\nexport interface IEntityProjectionProvider<ID = any> {\n upsertProjections(entries: EntityTaskProjection<ID>[]): Promise<void>;\n}\n\n/**\n * Configuration for entity projection behavior.\n */\nexport interface EntityProjectionConfig {\n /** Include task payload in projection (default: false for performance) */\n includePayload?: boolean;\n}\n\n/**\n * Sync entity projections to the provider. Non-fatal — logs and continues on error.\n */\nexport async function syncProjections<ID>(\n projections: EntityTaskProjection<ID>[],\n provider: IEntityProjectionProvider<ID> | undefined,\n logger: { error(msg: string): void }\n): Promise<void> {\n if (projections.length === 0 || !provider) return;\n try {\n await provider.upsertProjections(projections);\n } catch (err) {\n logger.error(`[TQ] Entity projection sync failed (non-fatal): ${err}`);\n }\n}\n\n/**\n * Build a single projection entry from a CronTask and target status.\n * Returns null if the task has no entity binding.\n * Throws if entity is present but task has no ID — fail-fast for developer errors.\n */\nexport function buildProjection<ID>(\n task: CronTask<ID>,\n status: EntityTaskProjectionStatus,\n options?: {\n includePayload?: boolean;\n error?: string;\n result?: unknown;\n }\n): EntityTaskProjection<ID> | null {\n if (!task.entity) return null;\n\n if (task.id == null) {\n throw new Error(\n `[TQ/RFC-003] Task with entity (${task.entity.type}:${task.entity.id}) has no task ID. ` +\n `Entity-bearing tasks must have an ID for projection keying. ` +\n `Set store_on_failure:true, force_store:true, or assign an ID before addTasks().`\n );\n }\n\n return {\n task_id: task.id,\n entity_id: task.entity.id,\n entity_type: task.entity.type,\n task_type: task.type,\n queue_id: task.queue_id,\n status,\n payload: options?.includePayload ? task.payload : undefined,\n error: options?.error,\n result: options?.result,\n created_at: task.created_at || new Date(),\n updated_at: new Date(),\n };\n}","import {Logger, LogLevel} from \"@supergrowthai/utils\";\nimport {TaskStore} from \"../TaskStore.js\";\nimport {Actions} from \"../Actions.js\";\nimport {IMessageQueue, QueueName} from \"@supergrowthai/mq\";\nimport {tId} from \"../../utils/task-id-gen.js\";\nimport {CronTask} from \"../../adapters\";\nimport {TaskQueuesManager} from \"../TaskQueuesManager.js\";\nimport {computeRetryDecision} from \"./retry-utils.js\";\nimport type {IEntityProjectionProvider, EntityProjectionConfig, EntityTaskProjection} from \"../entity/IEntityProjectionProvider.js\";\nimport {buildProjection, syncProjections} from \"../entity/IEntityProjectionProvider.js\";\nimport type {FlowMiddleware} from \"../flow/FlowMiddleware.js\";\n\nconst logger = new Logger('AsyncActions', LogLevel.INFO);\n\n/**\n * Interface for emitting async task lifecycle events.\n * Constructed from the sync-path's ITaskLifecycleProvider by TaskRunner.\n */\nexport interface AsyncLifecycleEmitter {\n onCompleted(task: CronTask<any>, result?: unknown): void;\n onFailed(task: CronTask<any>, error: Error, willRetry: boolean): void;\n onScheduled?(task: CronTask<any>): void;\n}\n\nexport class AsyncActions<ID = any> {\n private readonly actions: Actions<ID>;\n private readonly taskId: string;\n\n constructor(\n private messageQueue: IMessageQueue<ID>,\n private taskStore: TaskStore<ID>,\n private taskQueue: TaskQueuesManager<ID>,\n actions: Actions<ID>,\n private task: CronTask<ID>,\n private generateId: () => ID,\n private lifecycleEmitter?: AsyncLifecycleEmitter,\n private entityProjection?: IEntityProjectionProvider<ID>,\n private entityProjectionConfig?: EntityProjectionConfig,\n private flowMiddleware?: FlowMiddleware<ID>\n ) {\n this.actions = actions;\n this.taskId = tId(task);\n }\n\n /**\n * Called when the async promise completes to execute the collected actions\n */\n async onPromiseFulfilled(): Promise<void> {\n // Extract this task's results (NO batch context for async tasks)\n const results = this.actions.extractTaskActions(this.taskId);\n\n // If task didn't call success or fail, default to fail (forgetful executor)\n const hasCompletion = results.successTasks.length > 0 || results.failedTasks.length > 0;\n if (!hasCompletion) {\n logger.warn(`Async task ${this.taskId} completed without calling success() or fail() — defaulting to fail`);\n results.failedTasks.push({\n ...this.task,\n execution_stats: {\n ...(this.task.execution_stats || {}),\n last_error: `Async task ${this.taskId} completed without calling success() or fail()`,\n }\n });\n }\n\n logger.info(`[AsyncActions] Processing results for async task ${this.taskId}: ` +\n `${results.successTasks.length} success, ${results.failedTasks.length} failed, ` +\n `${results.newTasks.length} new tasks`);\n\n // Process failed tasks with retry logic\n if (results.failedTasks.length > 0) {\n for (const failedTask of results.failedTasks) {\n try {\n await this.processFailedTaskWithRetry(failedTask);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to process failed task:`, err);\n throw err;\n }\n }\n }\n\n if (results.successTasks.length > 0) {\n try {\n await this.taskStore.markTasksAsSuccess(results.successTasks);\n logger.info(`[AsyncActions] Marked ${results.successTasks.length} tasks as success in database`);\n\n // Emit lifecycle event for each success\n if (this.lifecycleEmitter) {\n for (const task of results.successTasks) {\n try {\n this.lifecycleEmitter.onCompleted(task, task.execution_result);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onCompleted error:`, err);\n }\n }\n }\n\n // RFC-003: Emit 'executed' entity projections for async success tasks\n await syncProjections(\n results.successTasks\n .map(t => buildProjection(t, 'executed', {\n includePayload: this.entityProjectionConfig?.includePayload,\n result: t.execution_result,\n }))\n .filter((p): p is EntityTaskProjection<ID> => p !== null),\n this.entityProjection,\n logger\n );\n } catch (err) {\n logger.error(`[AsyncActions] Failed to mark tasks as success:`, err);\n throw err;\n }\n }\n\n // RFC-002: Flow middleware — process terminal tasks for barrier tracking and join dispatch\n if (this.flowMiddleware) {\n try {\n // Collect final failures (not retries) for flow middleware\n const finalFailedTasks: CronTask<ID>[] = [];\n for (const failedTask of results.failedTasks) {\n const executor = this.taskQueue.getExecutor(failedTask.queue_id, failedTask.type);\n const maxRetries = failedTask.retries ?? executor?.default_retries ?? 0;\n const decision = computeRetryDecision(failedTask, maxRetries);\n // Only include final failures (no more retries)\n if (decision.action !== 'retry' || !decision.retryTask) {\n finalFailedTasks.push(failedTask);\n }\n }\n\n if (results.successTasks.length > 0 || finalFailedTasks.length > 0) {\n const flowResult = await this.flowMiddleware.onPostProcess({\n successTasks: results.successTasks,\n failedTasks: finalFailedTasks,\n });\n\n if (flowResult.projections.length > 0 && this.entityProjection) {\n await syncProjections(flowResult.projections, this.entityProjection, logger);\n }\n\n if (flowResult.joinTasks.length > 0) {\n await this.scheduleNewTasks(flowResult.joinTasks);\n }\n }\n } catch (err) {\n logger.error(`[AsyncActions] Flow middleware failed (non-fatal): ${err}`);\n }\n }\n\n if (results.newTasks.length > 0) {\n logger.info(`[AsyncActions] Scheduling ${results.newTasks.length} new tasks`);\n await this.scheduleNewTasks(results.newTasks);\n }\n }\n\n /**\n * Process a failed task through the retry pipeline\n */\n private async processFailedTaskWithRetry(failedTask: CronTask<ID>): Promise<void> {\n const executor = this.taskQueue.getExecutor(failedTask.queue_id, failedTask.type);\n const maxRetries = failedTask.retries ?? executor?.default_retries ?? 0;\n const decision = computeRetryDecision(failedTask, maxRetries);\n const willRetry = decision.action === 'retry' && !!decision.retryTask;\n\n if (willRetry) {\n logger.info(`[AsyncActions] Retrying async task ${this.taskId} (attempt ${(decision.retryTask!.execution_stats?.retry_count as number) || 0})`);\n await this.taskStore.updateTasksForRetry([decision.retryTask!]);\n } else {\n logger.info(`[AsyncActions] Async task ${this.taskId} exhausted retries, marking as failed`);\n await this.taskStore.markTasksAsFailed([failedTask]);\n\n // RFC-003: Emit 'failed' entity projection for final-failed async tasks\n const errorMsg = failedTask.execution_stats?.last_error as string || 'Task failed';\n const p = buildProjection(failedTask, 'failed', {\n includePayload: this.entityProjectionConfig?.includePayload,\n error: errorMsg,\n });\n if (p) await syncProjections([p], this.entityProjection, logger);\n }\n\n if (this.lifecycleEmitter) {\n const errorMsg = failedTask.execution_stats?.last_error as string || 'Task failed';\n try {\n this.lifecycleEmitter.onFailed(failedTask, new Error(errorMsg), willRetry);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onFailed error:`, err);\n }\n }\n }\n\n /**\n * Schedule new tasks - replicates the logic from task-handler's addTasks\n */\n private async scheduleNewTasks(tasks: CronTask<ID>[]): Promise<void> {\n const now = new Date();\n const immediate: { [key in QueueName]?: CronTask<ID>[] } = {};\n const future: CronTask<ID>[] = [];\n\n // Split tasks by timing\n for (const task of tasks) {\n const timeDiff = (task.execute_at.getTime() - now.getTime()) / 1000 / 60; // in minutes\n\n if (timeDiff > 2) {\n // Future task - goes to database\n future.push(task);\n } else {\n // Immediate task - goes to message queue\n const queue = task.queue_id;\n if (!immediate[queue]) {\n immediate[queue] = [];\n }\n immediate[queue].push(task);\n }\n }\n\n // Process immediate tasks\n const iQueues = Object.keys(immediate) as QueueName[];\n for (const queue of iQueues) {\n const queueTasks = immediate[queue]!.map((task) => {\n const executor = this.taskQueue.getExecutor(task.queue_id, task.type);\n const shouldStoreOnFailure = executor?.store_on_failure ?? false;\n const id = shouldStoreOnFailure ? {id: this.generateId()} : {};\n const partitionKey = executor?.getPartitionKey?.(task);\n return {...id, ...task, ...(partitionKey ? {partition_key: partitionKey} : {})};\n });\n\n // Entity projections for scheduled tasks (mirrors TaskHandler.addTasks pattern)\n if (this.entityProjection) {\n try {\n const projections = queueTasks\n .filter(t => t.entity)\n .map(t => buildProjection(t, 'scheduled', {includePayload: this.entityProjectionConfig?.includePayload}))\n .filter((p): p is EntityTaskProjection<ID> => p !== null);\n await syncProjections(projections, this.entityProjection, logger);\n } catch (err) {\n logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);\n }\n }\n\n try {\n await this.messageQueue.addMessages(queue, queueTasks as CronTask<ID>[]);\n logger.info(`[AsyncActions] Added ${queueTasks.length} immediate tasks to queue ${queue}`);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to add tasks to queue ${queue}:`, err);\n throw err;\n }\n\n // Emit onScheduled lifecycle event for each task\n if (this.lifecycleEmitter?.onScheduled) {\n for (const task of queueTasks) {\n try {\n this.lifecycleEmitter.onScheduled(task);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);\n }\n }\n }\n }\n\n // Process future tasks\n if (future.length > 0) {\n const futureTasks = future.map((task) => {\n const executor = this.taskQueue.getExecutor(task.queue_id, task.type);\n const shouldStoreOnFailure = executor?.store_on_failure ?? false;\n const id = shouldStoreOnFailure ? {id: this.generateId()} : {};\n const partitionKey = executor?.getPartitionKey?.(task);\n return {...id, ...task, ...(partitionKey ? {partition_key: partitionKey} : {})};\n });\n\n // Entity projections for future scheduled tasks\n if (this.entityProjection) {\n try {\n const projections = futureTasks\n .filter(t => t.entity)\n .map(t => buildProjection(t, 'scheduled', {includePayload: this.entityProjectionConfig?.includePayload}))\n .filter((p): p is EntityTaskProjection<ID> => p !== null);\n await syncProjections(projections, this.entityProjection, logger);\n } catch (err) {\n logger.error(`[AsyncActions] Entity projection failed (non-fatal): ${err}`);\n }\n }\n\n try {\n await this.taskStore.addTasksToScheduled(futureTasks);\n logger.info(`[AsyncActions] Added ${futureTasks.length} future tasks to database`);\n } catch (err) {\n logger.error(`[AsyncActions] Failed to add tasks to database:`, err);\n throw err;\n }\n\n // Emit onScheduled lifecycle event for future tasks\n if (this.lifecycleEmitter?.onScheduled) {\n for (const task of futureTasks) {\n try {\n this.lifecycleEmitter.onScheduled(task);\n } catch (err) {\n logger.error(`[AsyncActions] Lifecycle onScheduled error:`, err);\n }\n }\n }\n }\n }\n}\n"],"names":["logger","Logger","LogLevel","tId"],"mappings":"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"}
@@ -38,10 +38,12 @@ function enrichTaskWithError(task, error, meta) {
38
38
  return task;
39
39
  }
40
40
  class Actions {
41
- constructor(taskRunnerId) {
41
+ constructor(taskRunnerId, flowLifecycleProvider, workerId = "") {
42
42
  this.taskContexts = /* @__PURE__ */ new Map();
43
43
  this._flowProjections = [];
44
44
  this.taskRunnerId = taskRunnerId;
45
+ this.flowLifecycleProvider = flowLifecycleProvider;
46
+ this.workerId = workerId;
45
47
  this.log = logger.child({});
46
48
  }
47
49
  /**
@@ -233,6 +235,26 @@ class Actions {
233
235
  });
234
236
  }
235
237
  logger.info(`[${this.taskRunnerId}] Started flow ${flowId} with ${steps.length} steps, join: ${config.join.type}`);
238
+ if (this.flowLifecycleProvider?.onFlowStarted) {
239
+ try {
240
+ const result = this.flowLifecycleProvider.onFlowStarted({
241
+ flow_id: flowId,
242
+ total_steps: steps.length,
243
+ join: config.join,
244
+ failure_policy: failurePolicy,
245
+ entity: config.entity,
246
+ worker_id: this.workerId,
247
+ consumer_id: this.taskRunnerId,
248
+ started_at: now,
249
+ step_types: steps.map((s) => s.type)
250
+ });
251
+ if (result instanceof Promise) {
252
+ result.catch((err) => logger.error(`[TQ] Flow lifecycle onFlowStarted error: ${err}`));
253
+ }
254
+ } catch (err) {
255
+ logger.error(`[TQ] Flow lifecycle onFlowStarted error: ${err}`);
256
+ }
257
+ }
236
258
  return flowId;
237
259
  }
238
260
  /**