@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.
- package/README.md +149 -8
- package/dist/{AsyncActions-CZYO8ShR.js → AsyncActions-B8ImDgTo.js} +39 -3
- package/dist/AsyncActions-B8ImDgTo.js.map +1 -0
- package/dist/{AsyncActions-BOO1ikWz.cjs → AsyncActions-BsxMX_Ib.cjs} +39 -3
- package/dist/AsyncActions-BsxMX_Ib.cjs.map +1 -0
- package/dist/core/Actions.cjs +23 -1
- package/dist/core/Actions.cjs.map +1 -1
- package/dist/core/Actions.mjs +23 -1
- package/dist/core/Actions.mjs.map +1 -1
- package/dist/core/async/AsyncActions.cjs +1 -1
- package/dist/core/async/AsyncActions.mjs +1 -1
- package/dist/index.cjs +459 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +459 -226
- package/dist/index.mjs.map +1 -1
- package/dist/src/core/Actions.d.cts +5 -1
- package/dist/src/core/Actions.d.ts +5 -1
- package/dist/src/core/TaskHandler.d.cts +6 -0
- package/dist/src/core/TaskHandler.d.ts +6 -0
- package/dist/src/core/TaskRunner.d.cts +22 -5
- package/dist/src/core/TaskRunner.d.ts +22 -5
- package/dist/src/core/async/AsyncActions.d.cts +1 -0
- package/dist/src/core/async/AsyncActions.d.ts +1 -0
- package/dist/src/core/flow/FlowMiddleware.d.cts +6 -1
- package/dist/src/core/flow/FlowMiddleware.d.ts +6 -1
- package/dist/src/core/flow/IFlowBarrierProvider.d.cts +4 -0
- package/dist/src/core/flow/IFlowBarrierProvider.d.ts +4 -0
- package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.cts +1 -0
- package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.ts +1 -0
- package/dist/src/core/lifecycle.d.cts +98 -3
- package/dist/src/core/lifecycle.d.ts +98 -3
- package/dist/src/providers/ConsoleHealthProvider.d.cts +42 -2
- package/dist/src/providers/ConsoleHealthProvider.d.ts +42 -2
- package/dist/src/test/lifecycle-events.test.d.cts +31 -0
- package/dist/src/test/lifecycle-events.test.d.ts +31 -0
- package/package.json +2 -2
- package/dist/AsyncActions-BOO1ikWz.cjs.map +0 -1
- 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:
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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":";;AAEA,MAAM,qBAAqB,IAAI,KAAK;AAgB7B,SAAS,qBACZ,MACA,YACiB;AACjB,QAAM,aAAc,KAAK,mBAAmB,OAAO,KAAK,gBAAgB,gBAAgB,WAClF,KAAK,gBAAgB,cACrB;AAEN,MAAI,cAAc,YAAY;AAC1B,WAAO,EAAC,QAAQ,OAAA;AAAA,EACpB;AAEA,QAAM,iBAAiB,KAAK,IAAI,KAAK,eAAe,KAAM,CAAC;AAC3D,QAAM,kBAAkB,iBAAiB,KAAK,IAAI,aAAa,GAAG,CAAC;AACnE,QAAM,aAAa,KAAK,IAAI,iBAAiB,kBAAkB;AAC/D,QAAM,YAAY,KAAK,IAAA,IAAQ;AAE/B,SAAO;AAAA,IACH,QAAQ;AAAA,IACR,WAAW;AAAA,MACP,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,YAAY,IAAI,KAAK,SAAS;AAAA,MAC9B,iBAAiB;AAAA,QACb,GAAI,KAAK,mBAAmB,CAAA;AAAA,QAC5B,aAAa,aAAa;AAAA,MAAA;AAAA,IAC9B;AAAA,EACJ;AAER;ACHA,eAAsB,gBAClB,aACA,UACAA,SACa;AACb,MAAI,YAAY,WAAW,KAAK,CAAC,SAAU;AAC3C,MAAI;AACA,UAAM,SAAS,kBAAkB,WAAW;AAAA,EAChD,SAAS,KAAK;AACV,IAAAA,QAAO,MAAM,mDAAmD,GAAG,EAAE;AAAA,EACzE;AACJ;AAOO,SAAS,gBACZ,MACA,QACA,SAK+B;AAC/B,MAAI,CAAC,KAAK,OAAQ,QAAO;AAEzB,MAAI,KAAK,MAAM,MAAM;AACjB,UAAM,IAAI;AAAA,MACN,kCAAkC,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE;AAAA,IAAA;AAAA,EAI5E;AAEA,SAAO;AAAA,IACH,SAAS,KAAK;AAAA,IACd,WAAW,KAAK,OAAO;AAAA,IACvB,aAAa,KAAK,OAAO;AAAA,IACzB,WAAW,KAAK;AAAA,IAChB,UAAU,KAAK;AAAA,IACf;AAAA,IACA,SAAS,SAAS,iBAAiB,KAAK,UAAU;AAAA,IAClD,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS;AAAA,IACjB,YAAY,KAAK,cAAc,oBAAI,KAAA;AAAA,IACnC,gCAAgB,KAAA;AAAA,EAAK;AAE7B;AClFA,MAAM,SAAS,IAAI,OAAO,gBAAgB,SAAS,IAAI;AAYhD,MAAM,aAAuB;AAAA,EAIhC,YACY,cACA,WACA,WACR,SACQ,MACA,YACA,kBACA,kBACA,wBACA,gBACV;AAVU,SAAA,eAAA;AACA,SAAA,YAAA;AACA,SAAA,YAAA;AAEA,SAAA,OAAA;AACA,SAAA,aAAA;AACA,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,yBAAA;AACA,SAAA,iBAAA;AAER,SAAK,UAAU;AACf,SAAK,SAAS,IAAI,IAAI;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,qBAAoC;AAEtC,UAAM,UAAU,KAAK,QAAQ,mBAAmB,KAAK,MAAM;AAG3D,UAAM,gBAAgB,QAAQ,aAAa,SAAS,KAAK,QAAQ,YAAY,SAAS;AACtF,QAAI,CAAC,eAAe;AAChB,aAAO,KAAK,cAAc,KAAK,MAAM,qEAAqE;AAC1G,cAAQ,YAAY,KAAK;AAAA,QACrB,GAAG,KAAK;AAAA,QACR,iBAAiB;AAAA,UACb,GAAI,KAAK,KAAK,mBAAmB,CAAA;AAAA,UACjC,YAAY,cAAc,KAAK,MAAM;AAAA,QAAA;AAAA,MACzC,CACH;AAAA,IACL;AAEA,WAAO,KAAK,oDAAoD,KAAK,MAAM,KACpE,QAAQ,aAAa,MAAM,aAAa,QAAQ,YAAY,MAAM,YAClE,QAAQ,SAAS,MAAM,YAAY;AAG1C,QAAI,QAAQ,YAAY,SAAS,GAAG;AAChC,iBAAW,cAAc,QAAQ,aAAa;AAC1C,YAAI;AACA,gBAAM,KAAK,2BAA2B,UAAU;AAAA,QACpD,SAAS,KAAK;AACV,iBAAO,MAAM,iDAAiD,GAAG;AACjE,gBAAM;AAAA,QACV;AAAA,MACJ;AAAA,IACJ;AAEA,QAAI,QAAQ,aAAa,SAAS,GAAG;AACjC,UAAI;AACA,cAAM,KAAK,UAAU,mBAAmB,QAAQ,YAAY;AAC5D,eAAO,KAAK,yBAAyB,QAAQ,aAAa,MAAM,+BAA+B;AAG/F,YAAI,KAAK,kBAAkB;AACvB,qBAAW,QAAQ,QAAQ,cAAc;AACrC,gBAAI;AACA,mBAAK,iBAAiB,YAAY,MAAM,KAAK,gBAAgB;AAAA,YACjE,SAAS,KAAK;AACV,qBAAO,MAAM,+CAA+C,GAAG;AAAA,YACnE;AAAA,UACJ;AAAA,QACJ;AAGA,cAAM;AAAA,UACF,QAAQ,aACH,IAAI,CAAA,MAAK,gBAAgB,GAAG,YAAY;AAAA,YACrC,gBAAgB,KAAK,wBAAwB;AAAA,YAC7C,QAAQ,EAAE;AAAA,UAAA,CACb,CAAC,EACD,OAAO,CAAC,MAAqC,MAAM,IAAI;AAAA,UAC5D,KAAK;AAAA,UACL;AAAA,QAAA;AAAA,MAER,SAAS,KAAK;AACV,eAAO,MAAM,mDAAmD,GAAG;AACnE,cAAM;AAAA,MACV;AAAA,IACJ;AAGA,QAAI,KAAK,gBAAgB;AACrB,UAAI;AAEA,cAAM,mBAAmC,CAAA;AACzC,mBAAW,cAAc,QAAQ,aAAa;AAC1C,gBAAM,WAAW,KAAK,UAAU,YAAY,WAAW,UAAU,WAAW,IAAI;AAChF,gBAAM,aAAa,WAAW,WAAW,UAAU,mBAAmB;AACtE,gBAAM,WAAW,qBAAqB,YAAY,UAAU;AAE5D,cAAI,SAAS,WAAW,WAAW,CAAC,SAAS,WAAW;AACpD,6BAAiB,KAAK,UAAU;AAAA,UACpC;AAAA,QACJ;AAEA,YAAI,QAAQ,aAAa,SAAS,KAAK,iBAAiB,SAAS,GAAG;AAChE,gBAAM,aAAa,MAAM,KAAK,eAAe,cAAc;AAAA,YACvD,cAAc,QAAQ;AAAA,YACtB,aAAa;AAAA,UAAA,CAChB;AAED,cAAI,WAAW,YAAY,SAAS,KAAK,KAAK,kBAAkB;AAC5D,kBAAM,gBAAgB,WAAW,aAAa,KAAK,kBAAkB,MAAM;AAAA,UAC/E;AAEA,cAAI,WAAW,UAAU,SAAS,GAAG;AACjC,kBAAM,KAAK,iBAAiB,WAAW,SAAS;AAAA,UACpD;AAAA,QACJ;AAAA,MACJ,SAAS,KAAK;AACV,eAAO,MAAM,sDAAsD,GAAG,EAAE;AAAA,MAC5E;AAAA,IACJ;AAEA,QAAI,QAAQ,SAAS,SAAS,GAAG;AAC7B,aAAO,KAAK,6BAA6B,QAAQ,SAAS,MAAM,YAAY;AAC5E,YAAM,KAAK,iBAAiB,QAAQ,QAAQ;AAAA,IAChD;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,2BAA2B,YAAyC;AAC9E,UAAM,WAAW,KAAK,UAAU,YAAY,WAAW,UAAU,WAAW,IAAI;AAChF,UAAM,aAAa,WAAW,WAAW,UAAU,mBAAmB;AACtE,UAAM,WAAW,qBAAqB,YAAY,UAAU;AAC5D,UAAM,YAAY,SAAS,WAAW,WAAW,CAAC,CAAC,SAAS;AAE5D,QAAI,WAAW;AACX,aAAO,KAAK,sCAAsC,KAAK,MAAM,aAAc,SAAS,UAAW,iBAAiB,eAA0B,CAAC,GAAG;AAC9I,YAAM,KAAK,UAAU,oBAAoB,CAAC,SAAS,SAAU,CAAC;AAAA,IAClE,OAAO;AACH,aAAO,KAAK,6BAA6B,KAAK,MAAM,uCAAuC;AAC3F,YAAM,KAAK,UAAU,kBAAkB,CAAC,UAAU,CAAC;AAGnD,YAAM,WAAW,WAAW,iBAAiB,cAAwB;AACrE,YAAM,IAAI,gBAAgB,YAAY,UAAU;AAAA,QAC5C,gBAAgB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,MAAA,CACV;AACD,UAAI,SAAS,gBAAgB,CAAC,CAAC,GAAG,KAAK,kBAAkB,MAAM;AAAA,IACnE;AAEA,QAAI,KAAK,kBAAkB;AACvB,YAAM,WAAW,WAAW,iBAAiB,cAAwB;AACrE,UAAI;AACA,aAAK,iBAAiB,SAAS,YAAY,IAAI,MAAM,QAAQ,GAAG,SAAS;AAAA,MAC7E,SAAS,KAAK;AACV,eAAO,MAAM,4CAA4C,GAAG;AAAA,MAChE;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAAiB,OAAsC;AACjE,UAAM,0BAAU,KAAA;AAChB,UAAM,YAAqD,CAAA;AAC3D,UAAM,SAAyB,CAAA;AAG/B,eAAW,QAAQ,OAAO;AACtB,YAAM,YAAY,KAAK,WAAW,QAAA,IAAY,IAAI,aAAa,MAAO;AAEtE,UAAI,WAAW,GAAG;AAEd,eAAO,KAAK,IAAI;AAAA,MACpB,OAAO;AAEH,cAAM,QAAQ,KAAK;AACnB,YAAI,CAAC,UAAU,KAAK,GAAG;AACnB,oBAAU,KAAK,IAAI,CAAA;AAAA,QACvB;AACA,kBAAU,KAAK,EAAE,KAAK,IAAI;AAAA,MAC9B;AAAA,IACJ;AAGA,UAAM,UAAU,OAAO,KAAK,SAAS;AACrC,eAAW,SAAS,SAAS;AACzB,YAAM,aAAa,UAAU,KAAK,EAAG,IAAI,CAAC,SAAS;AAC/C,cAAM,WAAW,KAAK,UAAU,YAAY,KAAK,UAAU,KAAK,IAAI;AACpE,cAAM,uBAAuB,UAAU,oBAAoB;AAC3D,cAAM,KAAK,uBAAuB,EAAC,IAAI,KAAK,WAAA,EAAW,IAAK,CAAA;AAC5D,cAAM,eAAe,UAAU,kBAAkB,IAAI;AACrD,eAAO,EAAC,GAAG,IAAI,GAAG,MAAM,GAAI,eAAe,EAAC,eAAe,aAAA,IAAgB,GAAC;AAAA,MAChF,CAAC;AAGD,UAAI,KAAK,kBAAkB;AACvB,YAAI;AACA,gBAAM,cAAc,WACf,OAAO,CAAA,MAAK,EAAE,MAAM,EACpB,IAAI,CAAA,MAAK,gBAAgB,GAAG,aAAa,EAAC,gBAAgB,KAAK,wBAAwB,eAAA,CAAe,CAAC,EACvG,OAAO,CAAC,MAAqC,MAAM,IAAI;AAC5D,gBAAM,gBAAgB,aAAa,KAAK,kBAAkB,MAAM;AAAA,QACpE,SAAS,KAAK;AACV,iBAAO,MAAM,wDAAwD,GAAG,EAAE;AAAA,QAC9E;AAAA,MACJ;AAEA,UAAI;AACA,cAAM,KAAK,aAAa,YAAY,OAAO,UAA4B;AACvE,eAAO,KAAK,wBAAwB,WAAW,MAAM,6BAA6B,KAAK,EAAE;AAAA,MAC7F,SAAS,KAAK;AACV,eAAO,MAAM,+CAA+C,KAAK,KAAK,GAAG;AACzE,cAAM;AAAA,MACV;AAGA,UAAI,KAAK,kBAAkB,aAAa;AACpC,mBAAW,QAAQ,YAAY;AAC3B,cAAI;AACA,iBAAK,iBAAiB,YAAY,IAAI;AAAA,UAC1C,SAAS,KAAK;AACV,mBAAO,MAAM,+CAA+C,GAAG;AAAA,UACnE;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAGA,QAAI,OAAO,SAAS,GAAG;AACnB,YAAM,cAAc,OAAO,IAAI,CAAC,SAAS;AACrC,cAAM,WAAW,KAAK,UAAU,YAAY,KAAK,UAAU,KAAK,IAAI;AACpE,cAAM,uBAAuB,UAAU,oBAAoB;AAC3D,cAAM,KAAK,uBAAuB,EAAC,IAAI,KAAK,WAAA,EAAW,IAAK,CAAA;AAC5D,cAAM,eAAe,UAAU,kBAAkB,IAAI;AACrD,eAAO,EAAC,GAAG,IAAI,GAAG,MAAM,GAAI,eAAe,EAAC,eAAe,aAAA,IAAgB,GAAC;AAAA,MAChF,CAAC;AAGD,UAAI,KAAK,kBAAkB;AACvB,YAAI;AACA,gBAAM,cAAc,YACf,OAAO,CAAA,MAAK,EAAE,MAAM,EACpB,IAAI,CAAA,MAAK,gBAAgB,GAAG,aAAa,EAAC,gBAAgB,KAAK,wBAAwB,eAAA,CAAe,CAAC,EACvG,OAAO,CAAC,MAAqC,MAAM,IAAI;AAC5D,gBAAM,gBAAgB,aAAa,KAAK,kBAAkB,MAAM;AAAA,QACpE,SAAS,KAAK;AACV,iBAAO,MAAM,wDAAwD,GAAG,EAAE;AAAA,QAC9E;AAAA,MACJ;AAEA,UAAI;AACA,cAAM,KAAK,UAAU,oBAAoB,WAAW;AACpD,eAAO,KAAK,wBAAwB,YAAY,MAAM,2BAA2B;AAAA,MACrF,SAAS,KAAK;AACV,eAAO,MAAM,mDAAmD,GAAG;AACnE,cAAM;AAAA,MACV;AAGA,UAAI,KAAK,kBAAkB,aAAa;AACpC,mBAAW,QAAQ,aAAa;AAC5B,cAAI;AACA,iBAAK,iBAAiB,YAAY,IAAI;AAAA,UAC1C,SAAS,KAAK;AACV,mBAAO,MAAM,+CAA+C,GAAG;AAAA,UACnE;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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-
|
|
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":";;;AAEA,MAAM,qBAAqB,IAAI,KAAK;AAgB7B,SAAS,qBACZ,MACA,YACiB;AACjB,QAAM,aAAc,KAAK,mBAAmB,OAAO,KAAK,gBAAgB,gBAAgB,WAClF,KAAK,gBAAgB,cACrB;AAEN,MAAI,cAAc,YAAY;AAC1B,WAAO,EAAC,QAAQ,OAAA;AAAA,EACpB;AAEA,QAAM,iBAAiB,KAAK,IAAI,KAAK,eAAe,KAAM,CAAC;AAC3D,QAAM,kBAAkB,iBAAiB,KAAK,IAAI,aAAa,GAAG,CAAC;AACnE,QAAM,aAAa,KAAK,IAAI,iBAAiB,kBAAkB;AAC/D,QAAM,YAAY,KAAK,IAAA,IAAQ;AAE/B,SAAO;AAAA,IACH,QAAQ;AAAA,IACR,WAAW;AAAA,MACP,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,YAAY,IAAI,KAAK,SAAS;AAAA,MAC9B,iBAAiB;AAAA,QACb,GAAI,KAAK,mBAAmB,CAAA;AAAA,QAC5B,aAAa,aAAa;AAAA,MAAA;AAAA,IAC9B;AAAA,EACJ;AAER;ACHA,eAAsB,gBAClB,aACA,UACAA,SACa;AACb,MAAI,YAAY,WAAW,KAAK,CAAC,SAAU;AAC3C,MAAI;AACA,UAAM,SAAS,kBAAkB,WAAW;AAAA,EAChD,SAAS,KAAK;AACV,IAAAA,QAAO,MAAM,mDAAmD,GAAG,EAAE;AAAA,EACzE;AACJ;AAOO,SAAS,gBACZ,MACA,QACA,SAK+B;AAC/B,MAAI,CAAC,KAAK,OAAQ,QAAO;AAEzB,MAAI,KAAK,MAAM,MAAM;AACjB,UAAM,IAAI;AAAA,MACN,kCAAkC,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE;AAAA,IAAA;AAAA,EAI5E;AAEA,SAAO;AAAA,IACH,SAAS,KAAK;AAAA,IACd,WAAW,KAAK,OAAO;AAAA,IACvB,aAAa,KAAK,OAAO;AAAA,IACzB,WAAW,KAAK;AAAA,IAChB,UAAU,KAAK;AAAA,IACf;AAAA,IACA,SAAS,SAAS,iBAAiB,KAAK,UAAU;AAAA,IAClD,OAAO,SAAS;AAAA,IAChB,QAAQ,SAAS;AAAA,IACjB,YAAY,KAAK,cAAc,oBAAI,KAAA;AAAA,IACnC,gCAAgB,KAAA;AAAA,EAAK;AAE7B;AClFA,MAAM,SAAS,IAAIC,OAAAA,OAAO,gBAAgBC,OAAAA,SAAS,IAAI;AAYhD,MAAM,aAAuB;AAAA,EAIhC,YACY,cACA,WACA,WACR,SACQ,MACA,YACA,kBACA,kBACA,wBACA,gBACV;AAVU,SAAA,eAAA;AACA,SAAA,YAAA;AACA,SAAA,YAAA;AAEA,SAAA,OAAA;AACA,SAAA,aAAA;AACA,SAAA,mBAAA;AACA,SAAA,mBAAA;AACA,SAAA,yBAAA;AACA,SAAA,iBAAA;AAER,SAAK,UAAU;AACf,SAAK,SAASC,gBAAAA,IAAI,IAAI;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,qBAAoC;AAEtC,UAAM,UAAU,KAAK,QAAQ,mBAAmB,KAAK,MAAM;AAG3D,UAAM,gBAAgB,QAAQ,aAAa,SAAS,KAAK,QAAQ,YAAY,SAAS;AACtF,QAAI,CAAC,eAAe;AAChB,aAAO,KAAK,cAAc,KAAK,MAAM,qEAAqE;AAC1G,cAAQ,YAAY,KAAK;AAAA,QACrB,GAAG,KAAK;AAAA,QACR,iBAAiB;AAAA,UACb,GAAI,KAAK,KAAK,mBAAmB,CAAA;AAAA,UACjC,YAAY,cAAc,KAAK,MAAM;AAAA,QAAA;AAAA,MACzC,CACH;AAAA,IACL;AAEA,WAAO,KAAK,oDAAoD,KAAK,MAAM,KACpE,QAAQ,aAAa,MAAM,aAAa,QAAQ,YAAY,MAAM,YAClE,QAAQ,SAAS,MAAM,YAAY;AAG1C,QAAI,QAAQ,YAAY,SAAS,GAAG;AAChC,iBAAW,cAAc,QAAQ,aAAa;AAC1C,YAAI;AACA,gBAAM,KAAK,2BAA2B,UAAU;AAAA,QACpD,SAAS,KAAK;AACV,iBAAO,MAAM,iDAAiD,GAAG;AACjE,gBAAM;AAAA,QACV;AAAA,MACJ;AAAA,IACJ;AAEA,QAAI,QAAQ,aAAa,SAAS,GAAG;AACjC,UAAI;AACA,cAAM,KAAK,UAAU,mBAAmB,QAAQ,YAAY;AAC5D,eAAO,KAAK,yBAAyB,QAAQ,aAAa,MAAM,+BAA+B;AAG/F,YAAI,KAAK,kBAAkB;AACvB,qBAAW,QAAQ,QAAQ,cAAc;AACrC,gBAAI;AACA,mBAAK,iBAAiB,YAAY,MAAM,KAAK,gBAAgB;AAAA,YACjE,SAAS,KAAK;AACV,qBAAO,MAAM,+CAA+C,GAAG;AAAA,YACnE;AAAA,UACJ;AAAA,QACJ;AAGA,cAAM;AAAA,UACF,QAAQ,aACH,IAAI,CAAA,MAAK,gBAAgB,GAAG,YAAY;AAAA,YACrC,gBAAgB,KAAK,wBAAwB;AAAA,YAC7C,QAAQ,EAAE;AAAA,UAAA,CACb,CAAC,EACD,OAAO,CAAC,MAAqC,MAAM,IAAI;AAAA,UAC5D,KAAK;AAAA,UACL;AAAA,QAAA;AAAA,MAER,SAAS,KAAK;AACV,eAAO,MAAM,mDAAmD,GAAG;AACnE,cAAM;AAAA,MACV;AAAA,IACJ;AAGA,QAAI,KAAK,gBAAgB;AACrB,UAAI;AAEA,cAAM,mBAAmC,CAAA;AACzC,mBAAW,cAAc,QAAQ,aAAa;AAC1C,gBAAM,WAAW,KAAK,UAAU,YAAY,WAAW,UAAU,WAAW,IAAI;AAChF,gBAAM,aAAa,WAAW,WAAW,UAAU,mBAAmB;AACtE,gBAAM,WAAW,qBAAqB,YAAY,UAAU;AAE5D,cAAI,SAAS,WAAW,WAAW,CAAC,SAAS,WAAW;AACpD,6BAAiB,KAAK,UAAU;AAAA,UACpC;AAAA,QACJ;AAEA,YAAI,QAAQ,aAAa,SAAS,KAAK,iBAAiB,SAAS,GAAG;AAChE,gBAAM,aAAa,MAAM,KAAK,eAAe,cAAc;AAAA,YACvD,cAAc,QAAQ;AAAA,YACtB,aAAa;AAAA,UAAA,CAChB;AAED,cAAI,WAAW,YAAY,SAAS,KAAK,KAAK,kBAAkB;AAC5D,kBAAM,gBAAgB,WAAW,aAAa,KAAK,kBAAkB,MAAM;AAAA,UAC/E;AAEA,cAAI,WAAW,UAAU,SAAS,GAAG;AACjC,kBAAM,KAAK,iBAAiB,WAAW,SAAS;AAAA,UACpD;AAAA,QACJ;AAAA,MACJ,SAAS,KAAK;AACV,eAAO,MAAM,sDAAsD,GAAG,EAAE;AAAA,MAC5E;AAAA,IACJ;AAEA,QAAI,QAAQ,SAAS,SAAS,GAAG;AAC7B,aAAO,KAAK,6BAA6B,QAAQ,SAAS,MAAM,YAAY;AAC5E,YAAM,KAAK,iBAAiB,QAAQ,QAAQ;AAAA,IAChD;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,2BAA2B,YAAyC;AAC9E,UAAM,WAAW,KAAK,UAAU,YAAY,WAAW,UAAU,WAAW,IAAI;AAChF,UAAM,aAAa,WAAW,WAAW,UAAU,mBAAmB;AACtE,UAAM,WAAW,qBAAqB,YAAY,UAAU;AAC5D,UAAM,YAAY,SAAS,WAAW,WAAW,CAAC,CAAC,SAAS;AAE5D,QAAI,WAAW;AACX,aAAO,KAAK,sCAAsC,KAAK,MAAM,aAAc,SAAS,UAAW,iBAAiB,eAA0B,CAAC,GAAG;AAC9I,YAAM,KAAK,UAAU,oBAAoB,CAAC,SAAS,SAAU,CAAC;AAAA,IAClE,OAAO;AACH,aAAO,KAAK,6BAA6B,KAAK,MAAM,uCAAuC;AAC3F,YAAM,KAAK,UAAU,kBAAkB,CAAC,UAAU,CAAC;AAGnD,YAAM,WAAW,WAAW,iBAAiB,cAAwB;AACrE,YAAM,IAAI,gBAAgB,YAAY,UAAU;AAAA,QAC5C,gBAAgB,KAAK,wBAAwB;AAAA,QAC7C,OAAO;AAAA,MAAA,CACV;AACD,UAAI,SAAS,gBAAgB,CAAC,CAAC,GAAG,KAAK,kBAAkB,MAAM;AAAA,IACnE;AAEA,QAAI,KAAK,kBAAkB;AACvB,YAAM,WAAW,WAAW,iBAAiB,cAAwB;AACrE,UAAI;AACA,aAAK,iBAAiB,SAAS,YAAY,IAAI,MAAM,QAAQ,GAAG,SAAS;AAAA,MAC7E,SAAS,KAAK;AACV,eAAO,MAAM,4CAA4C,GAAG;AAAA,MAChE;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAAiB,OAAsC;AACjE,UAAM,0BAAU,KAAA;AAChB,UAAM,YAAqD,CAAA;AAC3D,UAAM,SAAyB,CAAA;AAG/B,eAAW,QAAQ,OAAO;AACtB,YAAM,YAAY,KAAK,WAAW,QAAA,IAAY,IAAI,aAAa,MAAO;AAEtE,UAAI,WAAW,GAAG;AAEd,eAAO,KAAK,IAAI;AAAA,MACpB,OAAO;AAEH,cAAM,QAAQ,KAAK;AACnB,YAAI,CAAC,UAAU,KAAK,GAAG;AACnB,oBAAU,KAAK,IAAI,CAAA;AAAA,QACvB;AACA,kBAAU,KAAK,EAAE,KAAK,IAAI;AAAA,MAC9B;AAAA,IACJ;AAGA,UAAM,UAAU,OAAO,KAAK,SAAS;AACrC,eAAW,SAAS,SAAS;AACzB,YAAM,aAAa,UAAU,KAAK,EAAG,IAAI,CAAC,SAAS;AAC/C,cAAM,WAAW,KAAK,UAAU,YAAY,KAAK,UAAU,KAAK,IAAI;AACpE,cAAM,uBAAuB,UAAU,oBAAoB;AAC3D,cAAM,KAAK,uBAAuB,EAAC,IAAI,KAAK,WAAA,EAAW,IAAK,CAAA;AAC5D,cAAM,eAAe,UAAU,kBAAkB,IAAI;AACrD,eAAO,EAAC,GAAG,IAAI,GAAG,MAAM,GAAI,eAAe,EAAC,eAAe,aAAA,IAAgB,GAAC;AAAA,MAChF,CAAC;AAGD,UAAI,KAAK,kBAAkB;AACvB,YAAI;AACA,gBAAM,cAAc,WACf,OAAO,CAAA,MAAK,EAAE,MAAM,EACpB,IAAI,CAAA,MAAK,gBAAgB,GAAG,aAAa,EAAC,gBAAgB,KAAK,wBAAwB,eAAA,CAAe,CAAC,EACvG,OAAO,CAAC,MAAqC,MAAM,IAAI;AAC5D,gBAAM,gBAAgB,aAAa,KAAK,kBAAkB,MAAM;AAAA,QACpE,SAAS,KAAK;AACV,iBAAO,MAAM,wDAAwD,GAAG,EAAE;AAAA,QAC9E;AAAA,MACJ;AAEA,UAAI;AACA,cAAM,KAAK,aAAa,YAAY,OAAO,UAA4B;AACvE,eAAO,KAAK,wBAAwB,WAAW,MAAM,6BAA6B,KAAK,EAAE;AAAA,MAC7F,SAAS,KAAK;AACV,eAAO,MAAM,+CAA+C,KAAK,KAAK,GAAG;AACzE,cAAM;AAAA,MACV;AAGA,UAAI,KAAK,kBAAkB,aAAa;AACpC,mBAAW,QAAQ,YAAY;AAC3B,cAAI;AACA,iBAAK,iBAAiB,YAAY,IAAI;AAAA,UAC1C,SAAS,KAAK;AACV,mBAAO,MAAM,+CAA+C,GAAG;AAAA,UACnE;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAGA,QAAI,OAAO,SAAS,GAAG;AACnB,YAAM,cAAc,OAAO,IAAI,CAAC,SAAS;AACrC,cAAM,WAAW,KAAK,UAAU,YAAY,KAAK,UAAU,KAAK,IAAI;AACpE,cAAM,uBAAuB,UAAU,oBAAoB;AAC3D,cAAM,KAAK,uBAAuB,EAAC,IAAI,KAAK,WAAA,EAAW,IAAK,CAAA;AAC5D,cAAM,eAAe,UAAU,kBAAkB,IAAI;AACrD,eAAO,EAAC,GAAG,IAAI,GAAG,MAAM,GAAI,eAAe,EAAC,eAAe,aAAA,IAAgB,GAAC;AAAA,MAChF,CAAC;AAGD,UAAI,KAAK,kBAAkB;AACvB,YAAI;AACA,gBAAM,cAAc,YACf,OAAO,CAAA,MAAK,EAAE,MAAM,EACpB,IAAI,CAAA,MAAK,gBAAgB,GAAG,aAAa,EAAC,gBAAgB,KAAK,wBAAwB,eAAA,CAAe,CAAC,EACvG,OAAO,CAAC,MAAqC,MAAM,IAAI;AAC5D,gBAAM,gBAAgB,aAAa,KAAK,kBAAkB,MAAM;AAAA,QACpE,SAAS,KAAK;AACV,iBAAO,MAAM,wDAAwD,GAAG,EAAE;AAAA,QAC9E;AAAA,MACJ;AAEA,UAAI;AACA,cAAM,KAAK,UAAU,oBAAoB,WAAW;AACpD,eAAO,KAAK,wBAAwB,YAAY,MAAM,2BAA2B;AAAA,MACrF,SAAS,KAAK;AACV,eAAO,MAAM,mDAAmD,GAAG;AACnE,cAAM;AAAA,MACV;AAGA,UAAI,KAAK,kBAAkB,aAAa;AACpC,mBAAW,QAAQ,aAAa;AAC5B,cAAI;AACA,iBAAK,iBAAiB,YAAY,IAAI;AAAA,UAC1C,SAAS,KAAK;AACV,mBAAO,MAAM,+CAA+C,GAAG;AAAA,UACnE;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;;;;"}
|
package/dist/core/Actions.cjs
CHANGED
|
@@ -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
|
/**
|