@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/dist/index.mjs CHANGED
@@ -48,7 +48,7 @@ import { getEnvironmentQueueName } from "@supergrowthai/mq";
48
48
  import { L as Logger, a as LogLevel } from "./client-dvHNt8qU.js";
49
49
  import { tId } from "./utils/task-id-gen.mjs";
50
50
  import { Actions } from "./core/Actions.mjs";
51
- import { b as buildProjection, s as syncProjections, A as AsyncActions } from "./AsyncActions-CZYO8ShR.js";
51
+ import { b as buildProjection, s as syncProjections, A as AsyncActions } from "./AsyncActions-B8ImDgTo.js";
52
52
  import { AsyncLocalStorage } from "node:async_hooks";
53
53
  import moment from "moment";
54
54
  import * as os from "os";
@@ -663,7 +663,22 @@ function getLogContext() {
663
663
  return als.getStore();
664
664
  }
665
665
  class TaskRunner {
666
- constructor(messageQueue, taskQueue, taskStore, cacheProvider, generateId, lifecycleProvider, lifecycleConfig, entityProjection, entityProjectionConfig, flowMiddleware) {
666
+ constructor(opts) {
667
+ this.taskStartTimes = /* @__PURE__ */ new Map();
668
+ const {
669
+ messageQueue,
670
+ taskQueue,
671
+ taskStore,
672
+ cacheProvider,
673
+ generateId,
674
+ lifecycleProvider,
675
+ lifecycleConfig,
676
+ entityProjection,
677
+ entityProjectionConfig,
678
+ flowMiddleware,
679
+ flowLifecycleProvider,
680
+ workerId = ""
681
+ } = opts;
667
682
  this.messageQueue = messageQueue;
668
683
  this.taskQueue = taskQueue;
669
684
  this.taskStore = taskStore;
@@ -673,7 +688,8 @@ class TaskRunner {
673
688
  this.entityProjection = entityProjection;
674
689
  this.entityProjectionConfig = entityProjectionConfig;
675
690
  this.flowMiddleware = flowMiddleware;
676
- this.taskStartTimes = /* @__PURE__ */ new Map();
691
+ this.flowLifecycleProvider = flowLifecycleProvider;
692
+ this.workerId = workerId;
677
693
  this.logger = new Logger("TaskRunner", LogLevel.INFO);
678
694
  this.lockManager = new LockManager(cacheProvider, {
679
695
  prefix: "task_lock_",
@@ -740,7 +756,7 @@ class TaskRunner {
740
756
  this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
741
757
  }
742
758
  }
743
- const actions = new Actions(taskRunnerId);
759
+ const actions = new Actions(taskRunnerId, this.flowLifecycleProvider, this.workerId);
744
760
  const asyncTasks = [];
745
761
  const processedTaskIds = /* @__PURE__ */ new Set();
746
762
  for (let i = 0; i < groupedTasksArray.length; i++) {
@@ -776,7 +792,20 @@ class TaskRunner {
776
792
  taskGroup.tasks.forEach((task) => processedTaskIds.add(tId(task)));
777
793
  this.logger.info(`[${taskRunnerId}] Processing ${taskGroup.tasks.length} tasks of type: ${taskGroup.type}`);
778
794
  if (executor.multiple) {
779
- const batchStore = this.buildBatchLogStore(taskGroup.tasks, taskRunnerId);
795
+ const batchStore = this.buildBatchLogStore(taskGroup.tasks, this.workerId);
796
+ const batchTaskContexts = taskGroup.tasks.map((t) => this.buildTaskContext(t, this.workerId, taskRunnerId));
797
+ const batchStartedAt = Date.now();
798
+ this.emitLifecycleEvent(
799
+ this.lifecycleProvider?.onTaskBatchStarted,
800
+ {
801
+ task_type: taskGroup.type,
802
+ queue_id: firstTask.queue_id,
803
+ tasks: batchTaskContexts,
804
+ worker_id: this.workerId,
805
+ consumer_id: taskRunnerId,
806
+ started_at: new Date(batchStartedAt)
807
+ }
808
+ );
780
809
  await runWithLogContext(
781
810
  batchStore,
782
811
  () => executor.onTasks(taskGroup.tasks, actions).catch((err) => {
@@ -788,19 +817,40 @@ class TaskRunner {
788
817
  }
789
818
  })
790
819
  );
820
+ const succeeded = [];
821
+ const failed = [];
822
+ for (const task of taskGroup.tasks) {
823
+ const status = actions.getTaskResultStatus(tId(task));
824
+ if (status === "success") succeeded.push(tId(task));
825
+ else if (status === "fail") failed.push(tId(task));
826
+ else failed.push(tId(task));
827
+ }
828
+ this.emitLifecycleEvent(
829
+ this.lifecycleProvider?.onTaskBatchCompleted,
830
+ {
831
+ task_type: taskGroup.type,
832
+ queue_id: firstTask.queue_id,
833
+ tasks: batchTaskContexts,
834
+ worker_id: this.workerId,
835
+ consumer_id: taskRunnerId,
836
+ succeeded,
837
+ failed,
838
+ duration_ms: Date.now() - batchStartedAt
839
+ }
840
+ );
791
841
  } else {
792
842
  if (executor.parallel) {
793
843
  const chunks = chunk(taskGroup.tasks, executor.chunkSize);
794
844
  this.logger.info(`[${taskRunnerId}] Processing in parallel chunks of ${executor.chunkSize}`);
795
845
  for (const taskChunk of chunks) {
796
846
  for (const task of taskChunk) {
797
- this.emitTaskStarted(task, taskRunnerId);
847
+ this.emitTaskStarted(task, this.workerId, taskRunnerId);
798
848
  }
799
849
  const chunkPromises = [];
800
850
  for (let j = 0; j < taskChunk.length; j++) {
801
851
  const task = taskChunk[j];
802
852
  const taskActions = actions.forkForTask(task);
803
- const logStore = this.buildLogStore(task, taskRunnerId);
853
+ const logStore = this.buildLogStore(task, this.workerId);
804
854
  chunkPromises.push(runWithLogContext(
805
855
  logStore,
806
856
  () => executor.onTask(task, taskActions).catch((err) => {
@@ -815,12 +865,12 @@ class TaskRunner {
815
865
  for (const task of taskChunk) {
816
866
  const resultStatus = actions.getTaskResultStatus(tId(task));
817
867
  if (resultStatus === "success") {
818
- this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(tId(task)));
868
+ this.emitTaskCompleted(task, this.workerId, actions.getTaskResult(tId(task)), taskRunnerId);
819
869
  } else if (resultStatus === "fail") {
820
870
  const retryCount = task.execution_stats?.retry_count || 0;
821
871
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
822
872
  const willRetry = retryCount < maxRetries;
823
- this.emitTaskFailed(task, taskRunnerId, new Error("Task failed"), willRetry);
873
+ this.emitTaskFailed(task, this.workerId, new Error("Task failed"), willRetry, void 0, taskRunnerId);
824
874
  }
825
875
  }
826
876
  }
@@ -829,9 +879,9 @@ class TaskRunner {
829
879
  for (let j = 0; j < taskGroup.tasks.length; j++) {
830
880
  const task = taskGroup.tasks[j];
831
881
  if (!timeoutMs) {
832
- this.emitTaskStarted(task, taskRunnerId);
882
+ this.emitTaskStarted(task, this.workerId, taskRunnerId);
833
883
  const taskActions = actions.forkForTask(task);
834
- const logStore = this.buildLogStore(task, taskRunnerId);
884
+ const logStore = this.buildLogStore(task, this.workerId);
835
885
  await runWithLogContext(
836
886
  logStore,
837
887
  () => executor.onTask(task, taskActions).catch((err) => {
@@ -843,18 +893,18 @@ class TaskRunner {
843
893
  );
844
894
  const resultStatus = actions.getTaskResultStatus(tId(task));
845
895
  if (resultStatus === "success") {
846
- this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(tId(task)));
896
+ this.emitTaskCompleted(task, this.workerId, actions.getTaskResult(tId(task)), taskRunnerId);
847
897
  } else if (resultStatus === "fail") {
848
898
  const retryCount = task.execution_stats?.retry_count || 0;
849
899
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
850
900
  const willRetry = retryCount < maxRetries;
851
- this.emitTaskFailed(task, taskRunnerId, new Error("Task failed"), willRetry);
901
+ this.emitTaskFailed(task, this.workerId, new Error("Task failed"), willRetry, void 0, taskRunnerId);
852
902
  }
853
903
  } else {
854
- this.emitTaskStarted(task, taskRunnerId);
904
+ this.emitTaskStarted(task, this.workerId, taskRunnerId);
855
905
  const startTime = Date.now();
856
906
  const taskActions = actions.forkForTask(task);
857
- const logStore = this.buildLogStore(task, taskRunnerId);
907
+ const logStore = this.buildLogStore(task, this.workerId);
858
908
  const taskPromise = runWithLogContext(
859
909
  logStore,
860
910
  () => executor.onTask(task, taskActions).catch((err) => {
@@ -877,12 +927,12 @@ class TaskRunner {
877
927
  if (result !== "~~~timeout") {
878
928
  const resultStatus = actions.getTaskResultStatus(tId(task));
879
929
  if (resultStatus === "success") {
880
- this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(tId(task)));
930
+ this.emitTaskCompleted(task, this.workerId, actions.getTaskResult(tId(task)), taskRunnerId);
881
931
  } else if (resultStatus === "fail") {
882
932
  const retryCount = task.execution_stats?.retry_count || 0;
883
933
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
884
934
  const willRetry = retryCount < maxRetries;
885
- this.emitTaskFailed(task, taskRunnerId, new Error("Task failed"), willRetry);
935
+ this.emitTaskFailed(task, this.workerId, new Error("Task failed"), willRetry, void 0, taskRunnerId);
886
936
  }
887
937
  }
888
938
  if (result === "~~~timeout") {
@@ -895,10 +945,18 @@ class TaskRunner {
895
945
  } else {
896
946
  const asyncLifecycleEmitter = this.lifecycleProvider ? {
897
947
  onCompleted: (t, result2) => {
898
- this.emitTaskCompleted(t, taskRunnerId, result2);
948
+ this.emitTaskCompleted(t, this.workerId, result2, taskRunnerId);
899
949
  },
900
950
  onFailed: (t, error, willRetry) => {
901
- this.emitTaskFailed(t, taskRunnerId, error, willRetry);
951
+ this.emitTaskFailed(t, this.workerId, error, willRetry, void 0, taskRunnerId);
952
+ },
953
+ onScheduled: (t) => {
954
+ if (this.lifecycleProvider?.onTaskScheduled) {
955
+ this.emitLifecycleEvent(
956
+ this.lifecycleProvider.onTaskScheduled,
957
+ this.buildTaskContext(t, this.workerId, taskRunnerId)
958
+ );
959
+ }
902
960
  }
903
961
  } : void 0;
904
962
  const asyncActions = new AsyncActions(this.messageQueue, this.taskStore, this.taskQueue, actions, task, this.generateId, asyncLifecycleEmitter, this.entityProjection, this.entityProjectionConfig, this.flowMiddleware);
@@ -946,7 +1004,7 @@ class TaskRunner {
946
1004
  this.logger.error(`[TQ] Lifecycle callback error: ${err}`);
947
1005
  }
948
1006
  }
949
- buildTaskContext(task, workerId) {
1007
+ buildTaskContext(task, workerId, consumerId) {
950
1008
  const retryCount = task.execution_stats && typeof task.execution_stats.retry_count === "number" ? task.execution_stats.retry_count : 0;
951
1009
  const executor = this.taskQueue.getExecutor(task.queue_id, task.type);
952
1010
  const maxRetries = task.retries ?? executor?.default_retries ?? 0;
@@ -961,14 +1019,15 @@ class TaskRunner {
961
1019
  max_retries: maxRetries,
962
1020
  scheduled_at: task.created_at || /* @__PURE__ */ new Date(),
963
1021
  worker_id: workerId,
1022
+ consumer_id: consumerId,
964
1023
  log_context: task.metadata?.log_context
965
1024
  };
966
1025
  }
967
- emitTaskStarted(task, workerId) {
1026
+ emitTaskStarted(task, workerId, consumerId) {
968
1027
  const startedAt = Date.now();
969
1028
  this.taskStartTimes.set(tId(task), startedAt);
970
1029
  if (this.lifecycleProvider?.onTaskStarted) {
971
- const ctx = this.buildTaskContext(task, workerId);
1030
+ const ctx = this.buildTaskContext(task, workerId, consumerId);
972
1031
  const queuedDuration = startedAt - (task.created_at?.getTime() || startedAt);
973
1032
  this.emitLifecycleEvent(
974
1033
  this.lifecycleProvider.onTaskStarted,
@@ -980,12 +1039,12 @@ class TaskRunner {
980
1039
  );
981
1040
  }
982
1041
  }
983
- emitTaskCompleted(task, workerId, result) {
1042
+ emitTaskCompleted(task, workerId, result, consumerId) {
984
1043
  const completedAt = Date.now();
985
1044
  const startedAt = this.taskStartTimes.get(tId(task)) || completedAt;
986
1045
  this.taskStartTimes.delete(tId(task));
987
1046
  if (this.lifecycleProvider?.onTaskCompleted) {
988
- const ctx = this.buildTaskContext(task, workerId);
1047
+ const ctx = this.buildTaskContext(task, workerId, consumerId);
989
1048
  const timing = {
990
1049
  queued_duration_ms: startedAt - (task.created_at?.getTime() || startedAt),
991
1050
  processing_duration_ms: completedAt - startedAt,
@@ -997,12 +1056,12 @@ class TaskRunner {
997
1056
  );
998
1057
  }
999
1058
  }
1000
- emitTaskFailed(task, workerId, error, willRetry, nextAttemptAt) {
1059
+ emitTaskFailed(task, workerId, error, willRetry, nextAttemptAt, consumerId) {
1001
1060
  const completedAt = Date.now();
1002
1061
  const startedAt = this.taskStartTimes.get(tId(task)) || completedAt;
1003
1062
  this.taskStartTimes.delete(tId(task));
1004
1063
  if (this.lifecycleProvider?.onTaskFailed) {
1005
- const ctx = this.buildTaskContext(task, workerId);
1064
+ const ctx = this.buildTaskContext(task, workerId, consumerId);
1006
1065
  const timing = {
1007
1066
  queued_duration_ms: startedAt - (task.created_at?.getTime() || startedAt),
1008
1067
  processing_duration_ms: completedAt - startedAt,
@@ -1020,6 +1079,247 @@ function getEnabledQueues() {
1020
1079
  if (enabledQueues.length === 0) throw new Error("No queues enabled");
1021
1080
  return enabledQueues.map(getEnvironmentQueueName);
1022
1081
  }
1082
+ function getFlowMeta(task) {
1083
+ return task.metadata?.flow_meta;
1084
+ }
1085
+ function getFlowMetaRequired(task) {
1086
+ return task.metadata.flow_meta;
1087
+ }
1088
+ class FlowMiddleware {
1089
+ constructor(barrierProvider, generateId, flowLifecycleProvider, workerId = "") {
1090
+ this.barrierProvider = barrierProvider;
1091
+ this.generateId = generateId;
1092
+ this.flowLifecycleProvider = flowLifecycleProvider;
1093
+ this.workerId = workerId;
1094
+ }
1095
+ emitFlowEvent(callback, ctx) {
1096
+ if (!callback) return;
1097
+ try {
1098
+ const result = callback(ctx);
1099
+ if (result instanceof Promise) {
1100
+ result.catch(() => {
1101
+ });
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ }
1106
+ buildFlowContext(flowMeta) {
1107
+ return {
1108
+ flow_id: flowMeta.flow_id,
1109
+ total_steps: flowMeta.total_steps,
1110
+ join: flowMeta.join,
1111
+ failure_policy: flowMeta.failure_policy,
1112
+ entity: flowMeta.entity,
1113
+ worker_id: this.workerId
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Process completed tasks for flow orchestration.
1118
+ * Called from TaskHandler.postProcessTasks after markFailed/markSuccess.
1119
+ *
1120
+ * @param input Categorized terminal tasks — success and final-failed (no retries left)
1121
+ * @returns Join tasks to dispatch and entity projections to sync
1122
+ */
1123
+ async onPostProcess(input) {
1124
+ const joinTasks = [];
1125
+ const projections = [];
1126
+ const successTaskSet = new Set(input.successTasks);
1127
+ const allTasks = [...input.successTasks, ...input.failedTasks];
1128
+ const joinCompletions = [];
1129
+ const timeoutTasks = [];
1130
+ const stepTasks = [];
1131
+ for (const task of allTasks) {
1132
+ const flowMeta = getFlowMeta(task);
1133
+ if (!flowMeta) continue;
1134
+ const isSuccess = successTaskSet.has(task);
1135
+ if (flowMeta.is_join) {
1136
+ joinCompletions.push(task);
1137
+ } else if (flowMeta.is_timeout) {
1138
+ timeoutTasks.push(task);
1139
+ } else {
1140
+ stepTasks.push({ task, isSuccess });
1141
+ }
1142
+ }
1143
+ for (const task of joinCompletions) {
1144
+ const flowMeta = getFlowMetaRequired(task);
1145
+ const isSuccess = successTaskSet.has(task);
1146
+ if (flowMeta.entity) {
1147
+ try {
1148
+ const status = isSuccess ? "executed" : "failed";
1149
+ const error = !isSuccess ? task.execution_stats?.last_error || "Join task failed" : void 0;
1150
+ const p = buildProjection(
1151
+ {
1152
+ ...task,
1153
+ id: flowMeta.flow_id,
1154
+ entity: flowMeta.entity
1155
+ },
1156
+ status,
1157
+ { result: task.execution_result, error }
1158
+ );
1159
+ if (p) projections.push(p);
1160
+ } catch {
1161
+ }
1162
+ }
1163
+ }
1164
+ for (const task of timeoutTasks) {
1165
+ const flowMeta = getFlowMetaRequired(task);
1166
+ const flowId = flowMeta.flow_id;
1167
+ await this.barrierProvider.initBarrier(flowId, flowMeta.total_steps);
1168
+ const isComplete = await this.barrierProvider.isComplete(flowId);
1169
+ if (isComplete) continue;
1170
+ const isFirstAbort = await this.barrierProvider.markAborted(flowId);
1171
+ if (!isFirstAbort) continue;
1172
+ const partialResults = await this.barrierProvider.getStepResults(flowId);
1173
+ const flowResults = {
1174
+ flow_id: flowId,
1175
+ steps: partialResults,
1176
+ timed_out: true
1177
+ };
1178
+ const joinTask = this.buildJoinTask(flowMeta, flowResults);
1179
+ joinTasks.push(joinTask);
1180
+ if (this.flowLifecycleProvider?.onFlowTimedOut) {
1181
+ const startedAt = await this.barrierProvider.getStartedAt(flowId);
1182
+ const durationMs = startedAt ? Date.now() - startedAt.getTime() : 0;
1183
+ this.emitFlowEvent(this.flowLifecycleProvider.onFlowTimedOut, {
1184
+ ...this.buildFlowContext(flowMeta),
1185
+ duration_ms: durationMs,
1186
+ steps_completed: partialResults.length
1187
+ });
1188
+ }
1189
+ if (flowMeta.entity) {
1190
+ try {
1191
+ const p = buildProjection(
1192
+ {
1193
+ ...joinTask,
1194
+ id: flowId,
1195
+ entity: flowMeta.entity
1196
+ },
1197
+ "failed",
1198
+ { error: "flow_timeout" }
1199
+ );
1200
+ if (p) projections.push(p);
1201
+ } catch {
1202
+ }
1203
+ }
1204
+ }
1205
+ const stepsByFlow = /* @__PURE__ */ new Map();
1206
+ for (const entry of stepTasks) {
1207
+ const flowMeta = getFlowMetaRequired(entry.task);
1208
+ const group = stepsByFlow.get(flowMeta.flow_id) || [];
1209
+ group.push(entry);
1210
+ stepsByFlow.set(flowMeta.flow_id, group);
1211
+ }
1212
+ for (const [flowId, entries] of stepsByFlow) {
1213
+ const firstFlowMeta = getFlowMetaRequired(entries[0].task);
1214
+ await this.barrierProvider.initBarrier(flowId, firstFlowMeta.total_steps);
1215
+ if (firstFlowMeta.failure_policy === "abort") {
1216
+ const hasFailure = entries.some((e) => !e.isSuccess);
1217
+ if (hasFailure) {
1218
+ const isFirstAbort = await this.barrierProvider.markAborted(flowId);
1219
+ if (isFirstAbort) {
1220
+ const stepResults2 = this.buildStepResults(entries);
1221
+ await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults2);
1222
+ const allResults = await this.barrierProvider.getStepResults(flowId);
1223
+ const flowResults = {
1224
+ flow_id: flowId,
1225
+ steps: allResults,
1226
+ aborted: true
1227
+ };
1228
+ const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
1229
+ joinTasks.push(joinTask);
1230
+ if (this.flowLifecycleProvider?.onFlowAborted) {
1231
+ const startedAt = await this.barrierProvider.getStartedAt(flowId);
1232
+ const durationMs = startedAt ? Date.now() - startedAt.getTime() : 0;
1233
+ const failedEntry = entries.find((e) => !e.isSuccess);
1234
+ const triggerIndex = failedEntry ? failedEntry.task.metadata?.flow_meta?.step_index ?? -1 : -1;
1235
+ this.emitFlowEvent(this.flowLifecycleProvider.onFlowAborted, {
1236
+ ...this.buildFlowContext(firstFlowMeta),
1237
+ duration_ms: durationMs,
1238
+ steps_completed: allResults.length,
1239
+ trigger_step_index: triggerIndex
1240
+ });
1241
+ }
1242
+ if (firstFlowMeta.entity) {
1243
+ try {
1244
+ const p = buildProjection(
1245
+ {
1246
+ ...joinTask,
1247
+ id: flowId,
1248
+ entity: firstFlowMeta.entity
1249
+ },
1250
+ "failed",
1251
+ { error: "flow_aborted" }
1252
+ );
1253
+ if (p) projections.push(p);
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ }
1258
+ continue;
1259
+ }
1260
+ }
1261
+ const stepResults = this.buildStepResults(entries);
1262
+ const { remaining } = await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults);
1263
+ if (remaining === 0) {
1264
+ const allResults = await this.barrierProvider.getStepResults(flowId);
1265
+ const flowResults = {
1266
+ flow_id: flowId,
1267
+ steps: allResults
1268
+ };
1269
+ const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
1270
+ joinTasks.push(joinTask);
1271
+ if (this.flowLifecycleProvider?.onFlowCompleted) {
1272
+ const startedAt = await this.barrierProvider.getStartedAt(flowId);
1273
+ const durationMs = startedAt ? Date.now() - startedAt.getTime() : 0;
1274
+ const stepsSucceeded = allResults.filter((r) => r.status === "success").length;
1275
+ const stepsFailed = allResults.filter((r) => r.status === "fail").length;
1276
+ this.emitFlowEvent(this.flowLifecycleProvider.onFlowCompleted, {
1277
+ ...this.buildFlowContext(firstFlowMeta),
1278
+ duration_ms: durationMs,
1279
+ steps_succeeded: stepsSucceeded,
1280
+ steps_failed: stepsFailed
1281
+ });
1282
+ }
1283
+ }
1284
+ }
1285
+ return { joinTasks, projections };
1286
+ }
1287
+ buildStepResults(entries) {
1288
+ return entries.map(({ task, isSuccess }) => {
1289
+ const flowMeta = getFlowMetaRequired(task);
1290
+ return {
1291
+ step_index: flowMeta.step_index,
1292
+ status: isSuccess ? "success" : "fail",
1293
+ result: task.execution_result,
1294
+ error: !isSuccess ? task.execution_stats?.last_error || "Step failed" : void 0
1295
+ };
1296
+ });
1297
+ }
1298
+ buildJoinTask(flowMeta, flowResults) {
1299
+ const now = /* @__PURE__ */ new Date();
1300
+ const joinFlowMeta = {
1301
+ ...flowMeta,
1302
+ is_join: true,
1303
+ is_timeout: void 0,
1304
+ step_index: -1
1305
+ };
1306
+ return {
1307
+ id: this.generateId(),
1308
+ type: flowMeta.join.type,
1309
+ queue_id: flowMeta.join.queue_id,
1310
+ payload: { flow_results: flowResults },
1311
+ execute_at: now,
1312
+ status: "scheduled",
1313
+ created_at: now,
1314
+ updated_at: now,
1315
+ force_store: true,
1316
+ metadata: {
1317
+ flow_meta: joinFlowMeta
1318
+ },
1319
+ ...flowMeta.entity ? { entity: flowMeta.entity } : {}
1320
+ };
1321
+ }
1322
+ }
1023
1323
  const METRICS_KEY_PREFIX = "task_metrics:";
1024
1324
  const DISCARDED_TASKS_KEY = `${METRICS_KEY_PREFIX}discarded_tasks`;
1025
1325
  const STATS_THRESHOLD = parseInt(process.env.TQ_STATS_THRESHOLD || "1000");
@@ -1046,23 +1346,35 @@ class TaskHandler {
1046
1346
  };
1047
1347
  this.totalProcessingMs = 0;
1048
1348
  this.queueStats = /* @__PURE__ */ new Map();
1349
+ this.consumerStatsMap = /* @__PURE__ */ new Map();
1049
1350
  this.logger = new Logger("TaskHandler", LogLevel.INFO);
1050
1351
  this.config = config || {};
1051
1352
  this.workerId = `${os.hostname()}-${process.pid}-${Date.now()}`;
1052
1353
  this.workerStartedAt = /* @__PURE__ */ new Date();
1053
1354
  this.taskStore = new TaskStore(databaseAdapter);
1054
- this.taskRunner = new TaskRunner(
1055
- messageQueue,
1056
- taskQueuesManager,
1057
- this.taskStore,
1058
- this.cacheAdapter,
1355
+ if (this.config.flowLifecycleProvider && !this.config.flowBarrierProvider) {
1356
+ throw new Error("[TQ] flowLifecycleProvider requires flowBarrierProvider — flow lifecycle events need flow orchestration enabled");
1357
+ }
1358
+ this.flowMiddleware = this.config.flowBarrierProvider ? new FlowMiddleware(
1359
+ this.config.flowBarrierProvider,
1059
1360
  databaseAdapter.generateId.bind(databaseAdapter),
1060
- this.config.lifecycleProvider,
1061
- this.config.lifecycle,
1062
- this.config.entityProjection,
1063
- this.config.entityProjectionConfig,
1064
- this.config.flowMiddleware
1065
- );
1361
+ this.config.flowLifecycleProvider,
1362
+ this.workerId
1363
+ ) : void 0;
1364
+ this.taskRunner = new TaskRunner({
1365
+ messageQueue,
1366
+ taskQueue: taskQueuesManager,
1367
+ taskStore: this.taskStore,
1368
+ cacheProvider: this.cacheAdapter,
1369
+ generateId: databaseAdapter.generateId.bind(databaseAdapter),
1370
+ lifecycleProvider: this.config.lifecycleProvider,
1371
+ lifecycleConfig: this.config.lifecycle,
1372
+ entityProjection: this.config.entityProjection,
1373
+ entityProjectionConfig: this.config.entityProjectionConfig,
1374
+ flowMiddleware: this.flowMiddleware,
1375
+ flowLifecycleProvider: this.config.flowLifecycleProvider,
1376
+ workerId: this.workerId
1377
+ });
1066
1378
  }
1067
1379
  // ============ Lifecycle Event Helpers ============
1068
1380
  get lifecycleProvider() {
@@ -1367,9 +1679,9 @@ class TaskHandler {
1367
1679
  this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
1368
1680
  }
1369
1681
  }
1370
- if (this.config.flowMiddleware) {
1682
+ if (this.flowMiddleware) {
1371
1683
  try {
1372
- const flowResult = await this.config.flowMiddleware.onPostProcess({ successTasks, failedTasks: finalFailedTasks });
1684
+ const flowResult = await this.flowMiddleware.onPostProcess({ successTasks, failedTasks: finalFailedTasks });
1373
1685
  if (flowResult.projections.length > 0 && this.entityProjectionProvider) {
1374
1686
  await syncProjections(flowResult.projections, this.entityProjectionProvider, this.logger);
1375
1687
  }
@@ -1392,6 +1704,7 @@ class TaskHandler {
1392
1704
  }
1393
1705
  const batchStartTime = Date.now();
1394
1706
  const taskTypes = [...new Set(tasks.map((t) => t.type))];
1707
+ this.registerConsumerIfNew(id, streamName);
1395
1708
  if (this.workerProvider?.onBatchStarted) {
1396
1709
  this.emitLifecycleEvent(
1397
1710
  this.workerProvider.onBatchStarted,
@@ -1468,6 +1781,7 @@ class TaskHandler {
1468
1781
  await this.reportQueueStats(streamName);
1469
1782
  const batchDuration = Date.now() - batchStartTime;
1470
1783
  this.updateWorkerStats(successTasks.length, failedTasks.length, batchDuration);
1784
+ this.updateConsumerStats(id, successTasks.length, failedTasks.length);
1471
1785
  if (this.workerProvider?.onBatchCompleted) {
1472
1786
  this.emitLifecycleEvent(
1473
1787
  this.workerProvider.onBatchCompleted,
@@ -1500,6 +1814,7 @@ class TaskHandler {
1500
1814
  this.processMatureTasks(abortSignal);
1501
1815
  abortSignal?.addEventListener("abort", () => {
1502
1816
  this.stopHeartbeat();
1817
+ this.emitAllConsumersStopped("shutdown");
1503
1818
  this.emitWorkerStopped("shutdown");
1504
1819
  });
1505
1820
  }
@@ -1535,7 +1850,8 @@ class TaskHandler {
1535
1850
  {
1536
1851
  ...this.buildWorkerInfo(),
1537
1852
  stats: { ...this.workerStats },
1538
- memory_usage_mb: memUsage.heapUsed / 1024 / 1024
1853
+ memory_usage_mb: memUsage.heapUsed / 1024 / 1024,
1854
+ active_consumers: this.getActiveConsumerStats()
1539
1855
  }
1540
1856
  );
1541
1857
  }
@@ -1561,6 +1877,58 @@ class TaskHandler {
1561
1877
  this.workerStats.avg_processing_ms = this.totalProcessingMs / this.workerStats.tasks_processed;
1562
1878
  }
1563
1879
  }
1880
+ // ============ Consumer Tracking ============
1881
+ registerConsumerIfNew(consumerId, queueId) {
1882
+ if (this.consumerStatsMap.has(consumerId)) return;
1883
+ const now = /* @__PURE__ */ new Date();
1884
+ this.consumerStatsMap.set(consumerId, {
1885
+ consumer_id: consumerId,
1886
+ queue_id: queueId,
1887
+ tasks_processed: 0,
1888
+ tasks_succeeded: 0,
1889
+ tasks_failed: 0,
1890
+ started_at: now
1891
+ });
1892
+ if (this.workerProvider?.onConsumerStarted) {
1893
+ this.emitLifecycleEvent(
1894
+ this.workerProvider.onConsumerStarted,
1895
+ {
1896
+ consumer_id: consumerId,
1897
+ queue_id: queueId,
1898
+ worker_id: this.workerId,
1899
+ started_at: now
1900
+ }
1901
+ );
1902
+ }
1903
+ }
1904
+ updateConsumerStats(consumerId, succeeded, failed) {
1905
+ const stats = this.consumerStatsMap.get(consumerId);
1906
+ if (!stats) return;
1907
+ stats.tasks_processed += succeeded + failed;
1908
+ stats.tasks_succeeded += succeeded;
1909
+ stats.tasks_failed += failed;
1910
+ stats.last_task_at = /* @__PURE__ */ new Date();
1911
+ }
1912
+ getActiveConsumerStats() {
1913
+ return Array.from(this.consumerStatsMap.values()).map(({ started_at, ...stats }) => stats);
1914
+ }
1915
+ emitAllConsumersStopped(reason) {
1916
+ if (!this.workerProvider?.onConsumerStopped) return;
1917
+ for (const entry of this.consumerStatsMap.values()) {
1918
+ const { started_at, ...stats } = entry;
1919
+ this.emitLifecycleEvent(
1920
+ this.workerProvider.onConsumerStopped,
1921
+ {
1922
+ consumer_id: entry.consumer_id,
1923
+ queue_id: entry.queue_id,
1924
+ worker_id: this.workerId,
1925
+ started_at,
1926
+ reason,
1927
+ stats
1928
+ }
1929
+ );
1930
+ }
1931
+ }
1564
1932
  emitLifecycleEvent(callback, ctx) {
1565
1933
  if (!callback) return;
1566
1934
  try {
@@ -1656,6 +2024,7 @@ class TaskHandler {
1656
2024
  attempt: retryCount + 1,
1657
2025
  max_retries: maxRetries,
1658
2026
  scheduled_at: task.created_at || /* @__PURE__ */ new Date(),
2027
+ worker_id: this.workerId,
1659
2028
  log_context: task.metadata?.log_context
1660
2029
  };
1661
2030
  }
@@ -1835,7 +2204,8 @@ class InMemoryFlowBarrierProvider {
1835
2204
  this.barriers.set(flowId, {
1836
2205
  remaining: totalSteps,
1837
2206
  status: "active",
1838
- results: /* @__PURE__ */ new Map()
2207
+ results: /* @__PURE__ */ new Map(),
2208
+ started_at: /* @__PURE__ */ new Date()
1839
2209
  });
1840
2210
  }
1841
2211
  async batchDecrementAndCheck(flowId, results) {
@@ -1879,190 +2249,10 @@ class InMemoryFlowBarrierProvider {
1879
2249
  if (!state) return false;
1880
2250
  return state.status === "complete";
1881
2251
  }
1882
- }
1883
- function getFlowMeta(task) {
1884
- return task.metadata?.flow_meta;
1885
- }
1886
- function getFlowMetaRequired(task) {
1887
- return task.metadata.flow_meta;
1888
- }
1889
- class FlowMiddleware {
1890
- constructor(barrierProvider, generateId) {
1891
- this.barrierProvider = barrierProvider;
1892
- this.generateId = generateId;
1893
- }
1894
- /**
1895
- * Process completed tasks for flow orchestration.
1896
- * Called from TaskHandler.postProcessTasks after markFailed/markSuccess.
1897
- *
1898
- * @param input Categorized terminal tasks — success and final-failed (no retries left)
1899
- * @returns Join tasks to dispatch and entity projections to sync
1900
- */
1901
- async onPostProcess(input) {
1902
- const joinTasks = [];
1903
- const projections = [];
1904
- const successTaskSet = new Set(input.successTasks);
1905
- const allTasks = [...input.successTasks, ...input.failedTasks];
1906
- const joinCompletions = [];
1907
- const timeoutTasks = [];
1908
- const stepTasks = [];
1909
- for (const task of allTasks) {
1910
- const flowMeta = getFlowMeta(task);
1911
- if (!flowMeta) continue;
1912
- const isSuccess = successTaskSet.has(task);
1913
- if (flowMeta.is_join) {
1914
- joinCompletions.push(task);
1915
- } else if (flowMeta.is_timeout) {
1916
- timeoutTasks.push(task);
1917
- } else {
1918
- stepTasks.push({ task, isSuccess });
1919
- }
1920
- }
1921
- for (const task of joinCompletions) {
1922
- const flowMeta = getFlowMetaRequired(task);
1923
- const isSuccess = successTaskSet.has(task);
1924
- if (flowMeta.entity) {
1925
- try {
1926
- const status = isSuccess ? "executed" : "failed";
1927
- const error = !isSuccess ? task.execution_stats?.last_error || "Join task failed" : void 0;
1928
- const p = buildProjection(
1929
- {
1930
- ...task,
1931
- id: flowMeta.flow_id,
1932
- entity: flowMeta.entity
1933
- },
1934
- status,
1935
- { result: task.execution_result, error }
1936
- );
1937
- if (p) projections.push(p);
1938
- } catch {
1939
- }
1940
- }
1941
- }
1942
- for (const task of timeoutTasks) {
1943
- const flowMeta = getFlowMetaRequired(task);
1944
- const flowId = flowMeta.flow_id;
1945
- await this.barrierProvider.initBarrier(flowId, flowMeta.total_steps);
1946
- const isComplete = await this.barrierProvider.isComplete(flowId);
1947
- if (isComplete) continue;
1948
- const isFirstAbort = await this.barrierProvider.markAborted(flowId);
1949
- if (!isFirstAbort) continue;
1950
- const partialResults = await this.barrierProvider.getStepResults(flowId);
1951
- const flowResults = {
1952
- flow_id: flowId,
1953
- steps: partialResults,
1954
- timed_out: true
1955
- };
1956
- const joinTask = this.buildJoinTask(flowMeta, flowResults);
1957
- joinTasks.push(joinTask);
1958
- if (flowMeta.entity) {
1959
- try {
1960
- const p = buildProjection(
1961
- {
1962
- ...joinTask,
1963
- id: flowId,
1964
- entity: flowMeta.entity
1965
- },
1966
- "failed",
1967
- { error: "flow_timeout" }
1968
- );
1969
- if (p) projections.push(p);
1970
- } catch {
1971
- }
1972
- }
1973
- }
1974
- const stepsByFlow = /* @__PURE__ */ new Map();
1975
- for (const entry of stepTasks) {
1976
- const flowMeta = getFlowMetaRequired(entry.task);
1977
- const group = stepsByFlow.get(flowMeta.flow_id) || [];
1978
- group.push(entry);
1979
- stepsByFlow.set(flowMeta.flow_id, group);
1980
- }
1981
- for (const [flowId, entries] of stepsByFlow) {
1982
- const firstFlowMeta = getFlowMetaRequired(entries[0].task);
1983
- await this.barrierProvider.initBarrier(flowId, firstFlowMeta.total_steps);
1984
- if (firstFlowMeta.failure_policy === "abort") {
1985
- const hasFailure = entries.some((e) => !e.isSuccess);
1986
- if (hasFailure) {
1987
- const isFirstAbort = await this.barrierProvider.markAborted(flowId);
1988
- if (isFirstAbort) {
1989
- const stepResults2 = this.buildStepResults(entries);
1990
- await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults2);
1991
- const allResults = await this.barrierProvider.getStepResults(flowId);
1992
- const flowResults = {
1993
- flow_id: flowId,
1994
- steps: allResults,
1995
- aborted: true
1996
- };
1997
- const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
1998
- joinTasks.push(joinTask);
1999
- if (firstFlowMeta.entity) {
2000
- try {
2001
- const p = buildProjection(
2002
- {
2003
- ...joinTask,
2004
- id: flowId,
2005
- entity: firstFlowMeta.entity
2006
- },
2007
- "failed",
2008
- { error: "flow_aborted" }
2009
- );
2010
- if (p) projections.push(p);
2011
- } catch {
2012
- }
2013
- }
2014
- }
2015
- continue;
2016
- }
2017
- }
2018
- const stepResults = this.buildStepResults(entries);
2019
- const { remaining } = await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults);
2020
- if (remaining === 0) {
2021
- const allResults = await this.barrierProvider.getStepResults(flowId);
2022
- const flowResults = {
2023
- flow_id: flowId,
2024
- steps: allResults
2025
- };
2026
- const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
2027
- joinTasks.push(joinTask);
2028
- }
2029
- }
2030
- return { joinTasks, projections };
2031
- }
2032
- buildStepResults(entries) {
2033
- return entries.map(({ task, isSuccess }) => {
2034
- const flowMeta = getFlowMetaRequired(task);
2035
- return {
2036
- step_index: flowMeta.step_index,
2037
- status: isSuccess ? "success" : "fail",
2038
- result: task.execution_result,
2039
- error: !isSuccess ? task.execution_stats?.last_error || "Step failed" : void 0
2040
- };
2041
- });
2042
- }
2043
- buildJoinTask(flowMeta, flowResults) {
2044
- const now = /* @__PURE__ */ new Date();
2045
- const joinFlowMeta = {
2046
- ...flowMeta,
2047
- is_join: true,
2048
- is_timeout: void 0,
2049
- step_index: -1
2050
- };
2051
- return {
2052
- id: this.generateId(),
2053
- type: flowMeta.join.type,
2054
- queue_id: flowMeta.join.queue_id,
2055
- payload: { flow_results: flowResults },
2056
- execute_at: now,
2057
- status: "scheduled",
2058
- created_at: now,
2059
- updated_at: now,
2060
- force_store: true,
2061
- metadata: {
2062
- flow_meta: joinFlowMeta
2063
- },
2064
- ...flowMeta.entity ? { entity: flowMeta.entity } : {}
2065
- };
2252
+ async getStartedAt(flowId) {
2253
+ const state = this.barriers.get(flowId);
2254
+ if (!state) return null;
2255
+ return state.started_at;
2066
2256
  }
2067
2257
  }
2068
2258
  class ConsoleHealthProvider {
@@ -2102,7 +2292,7 @@ class ConsoleHealthProvider {
2102
2292
  }
2103
2293
  onWorkerHeartbeat(info) {
2104
2294
  console.log(
2105
- `${this.prefix} Worker heartbeat: ${info.worker_id} - processed: ${info.stats.tasks_processed}, success: ${info.stats.tasks_succeeded}, failed: ${info.stats.tasks_failed}, avg: ${info.stats.avg_processing_ms.toFixed(2)}ms, memory: ${info.memory_usage_mb.toFixed(2)}MB`
2295
+ `${this.prefix} Worker heartbeat: ${info.worker_id} - processed: ${info.stats.tasks_processed}, success: ${info.stats.tasks_succeeded}, failed: ${info.stats.tasks_failed}, avg: ${info.stats.avg_processing_ms.toFixed(2)}ms, memory: ${info.memory_usage_mb.toFixed(2)}MB, consumers: ${info.active_consumers.length}`
2106
2296
  );
2107
2297
  }
2108
2298
  onWorkerStopped(info) {
@@ -2120,6 +2310,49 @@ class ConsoleHealthProvider {
2120
2310
  `${this.prefix} Batch completed: ${info.batch_size} tasks in ${info.duration_ms}ms - succeeded: ${info.succeeded}, failed: ${info.failed}`
2121
2311
  );
2122
2312
  }
2313
+ // ============ Consumer Lifecycle ============
2314
+ onConsumerStarted(info) {
2315
+ console.log(
2316
+ `${this.prefix} Consumer started: ${info.consumer_id} on worker ${info.worker_id} - queue: ${info.queue_id}`
2317
+ );
2318
+ }
2319
+ onConsumerStopped(info) {
2320
+ console.log(
2321
+ `${this.prefix} Consumer stopped: ${info.consumer_id} on worker ${info.worker_id} - reason: ${info.reason}, processed: ${info.stats.tasks_processed}, succeeded: ${info.stats.tasks_succeeded}, failed: ${info.stats.tasks_failed}`
2322
+ );
2323
+ }
2324
+ // ============ Task Batch Lifecycle ============
2325
+ onTaskBatchStarted(ctx) {
2326
+ console.log(
2327
+ `${this.prefix} Task batch started: ${ctx.task_type} - ${ctx.tasks.length} tasks in queue ${ctx.queue_id}`
2328
+ );
2329
+ }
2330
+ onTaskBatchCompleted(ctx) {
2331
+ console.log(
2332
+ `${this.prefix} Task batch completed: ${ctx.task_type} in ${ctx.duration_ms}ms - succeeded: ${ctx.succeeded.length}, failed: ${ctx.failed.length}`
2333
+ );
2334
+ }
2335
+ // ============ Flow Lifecycle ============
2336
+ onFlowStarted(ctx) {
2337
+ console.log(
2338
+ `${this.prefix} Flow started: ${ctx.flow_id} - ${ctx.total_steps} steps [${ctx.step_types.join(", ")}], join: ${ctx.join.type}, policy: ${ctx.failure_policy}`
2339
+ );
2340
+ }
2341
+ onFlowCompleted(ctx) {
2342
+ console.log(
2343
+ `${this.prefix} Flow completed: ${ctx.flow_id} in ${ctx.duration_ms}ms - succeeded: ${ctx.steps_succeeded}, failed: ${ctx.steps_failed}`
2344
+ );
2345
+ }
2346
+ onFlowAborted(ctx) {
2347
+ console.log(
2348
+ `${this.prefix} Flow aborted: ${ctx.flow_id} in ${ctx.duration_ms}ms - steps completed: ${ctx.steps_completed}, trigger: step ${ctx.trigger_step_index}`
2349
+ );
2350
+ }
2351
+ onFlowTimedOut(ctx) {
2352
+ console.log(
2353
+ `${this.prefix} Flow timed out: ${ctx.flow_id} in ${ctx.duration_ms}ms - steps completed: ${ctx.steps_completed}`
2354
+ );
2355
+ }
2123
2356
  }
2124
2357
  export {
2125
2358
  Actions,