@supergrowthai/tq 1.0.11 → 1.0.13

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 (100) hide show
  1. package/README.md +338 -20
  2. package/dist/AsyncActions-BOO1ikWz.cjs +241 -0
  3. package/dist/AsyncActions-BOO1ikWz.cjs.map +1 -0
  4. package/dist/AsyncActions-CZYO8ShR.js +242 -0
  5. package/dist/AsyncActions-CZYO8ShR.js.map +1 -0
  6. package/dist/{PrismaAdapter-CvM_XNtE.cjs → PrismaAdapter-CUIWhjms.cjs} +57 -81
  7. package/dist/PrismaAdapter-CUIWhjms.cjs.map +1 -0
  8. package/dist/{PrismaAdapter-Dy7MV090.js → PrismaAdapter-D5ACKPbS.js} +57 -81
  9. package/dist/PrismaAdapter-D5ACKPbS.js.map +1 -0
  10. package/dist/adapters/index.cjs +1 -1
  11. package/dist/adapters/index.mjs +1 -1
  12. package/dist/client-BxG7LzLv.cjs +90 -0
  13. package/dist/client-BxG7LzLv.cjs.map +1 -0
  14. package/dist/client-dvHNt8qU.js +91 -0
  15. package/dist/client-dvHNt8qU.js.map +1 -0
  16. package/dist/core/Actions.cjs +184 -16
  17. package/dist/core/Actions.cjs.map +1 -1
  18. package/dist/core/Actions.mjs +184 -16
  19. package/dist/core/Actions.mjs.map +1 -1
  20. package/dist/core/async/AsyncActions.cjs +4 -98
  21. package/dist/core/async/AsyncActions.cjs.map +1 -1
  22. package/dist/core/async/AsyncActions.mjs +4 -98
  23. package/dist/core/async/AsyncActions.mjs.map +1 -1
  24. package/dist/core/async/AsyncTaskManager.cjs +133 -22
  25. package/dist/core/async/AsyncTaskManager.cjs.map +1 -1
  26. package/dist/core/async/AsyncTaskManager.mjs +133 -22
  27. package/dist/core/async/AsyncTaskManager.mjs.map +1 -1
  28. package/dist/index.cjs +517 -35
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.mjs +517 -35
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/src/adapters/ITaskStorageAdapter.d.cts +0 -5
  33. package/dist/src/adapters/ITaskStorageAdapter.d.ts +0 -5
  34. package/dist/src/adapters/InMemoryAdapter.d.cts +0 -2
  35. package/dist/src/adapters/InMemoryAdapter.d.ts +0 -2
  36. package/dist/src/adapters/MongoDbAdapter.d.cts +0 -2
  37. package/dist/src/adapters/MongoDbAdapter.d.ts +0 -2
  38. package/dist/src/adapters/PrismaAdapter.d.cts +0 -2
  39. package/dist/src/adapters/PrismaAdapter.d.ts +0 -2
  40. package/dist/src/adapters/types.d.cts +7 -0
  41. package/dist/src/adapters/types.d.ts +7 -0
  42. package/dist/src/core/Actions.d.cts +25 -2
  43. package/dist/src/core/Actions.d.ts +25 -2
  44. package/dist/src/core/TaskHandler.d.cts +13 -5
  45. package/dist/src/core/TaskHandler.d.ts +13 -5
  46. package/dist/src/core/TaskRunner.d.cts +16 -1
  47. package/dist/src/core/TaskRunner.d.ts +16 -1
  48. package/dist/src/core/async/AsyncActions.d.cts +20 -1
  49. package/dist/src/core/async/AsyncActions.d.ts +20 -1
  50. package/dist/src/core/async/AsyncTaskManager.d.cts +36 -4
  51. package/dist/src/core/async/AsyncTaskManager.d.ts +36 -4
  52. package/dist/src/core/async/async-task-manager.d.cts +21 -3
  53. package/dist/src/core/async/async-task-manager.d.ts +21 -3
  54. package/dist/src/core/async/retry-utils.d.cts +15 -0
  55. package/dist/src/core/async/retry-utils.d.ts +15 -0
  56. package/dist/src/core/base/interfaces.d.cts +10 -2
  57. package/dist/src/core/base/interfaces.d.ts +10 -2
  58. package/dist/src/core/entity/IEntityProjectionProvider.d.cts +45 -0
  59. package/dist/src/core/entity/IEntityProjectionProvider.d.ts +45 -0
  60. package/dist/src/core/entity/index.d.cts +1 -0
  61. package/dist/src/core/entity/index.d.ts +1 -0
  62. package/dist/src/core/flow/FlowMiddleware.d.cts +26 -0
  63. package/dist/src/core/flow/FlowMiddleware.d.ts +26 -0
  64. package/dist/src/core/flow/IFlowBarrierProvider.d.cts +46 -0
  65. package/dist/src/core/flow/IFlowBarrierProvider.d.ts +46 -0
  66. package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.cts +10 -0
  67. package/dist/src/core/flow/InMemoryFlowBarrierProvider.d.ts +10 -0
  68. package/dist/src/core/flow/index.d.cts +4 -0
  69. package/dist/src/core/flow/index.d.ts +4 -0
  70. package/dist/src/core/flow/types.d.cts +82 -0
  71. package/dist/src/core/flow/types.d.ts +82 -0
  72. package/dist/src/core/lifecycle.d.cts +9 -4
  73. package/dist/src/core/lifecycle.d.ts +9 -4
  74. package/dist/src/core/log-context.d.cts +10 -0
  75. package/dist/src/core/log-context.d.ts +10 -0
  76. package/dist/src/index.d.cts +4 -0
  77. package/dist/src/index.d.ts +4 -0
  78. package/dist/src/test/adapter-consistency.test.d.cts +11 -0
  79. package/dist/src/test/adapter-consistency.test.d.ts +11 -0
  80. package/dist/src/test/immediate-mode-bugs.test.d.cts +11 -0
  81. package/dist/src/test/immediate-mode-bugs.test.d.ts +11 -0
  82. package/dist/src/test/rfc-001-result-persistence.test.d.cts +17 -0
  83. package/dist/src/test/rfc-001-result-persistence.test.d.ts +17 -0
  84. package/dist/src/test/rfc-002-flow-orchestration.test.d.cts +24 -0
  85. package/dist/src/test/rfc-002-flow-orchestration.test.d.ts +24 -0
  86. package/dist/src/test/rfc-003-entity-projection.test.d.cts +14 -0
  87. package/dist/src/test/rfc-003-entity-projection.test.d.ts +14 -0
  88. package/dist/src/test/rfc-004-async-hardening.test.d.cts +14 -0
  89. package/dist/src/test/rfc-004-async-hardening.test.d.ts +14 -0
  90. package/dist/src/test/rfc-005-log-context.test.d.cts +14 -0
  91. package/dist/src/test/rfc-005-log-context.test.d.ts +14 -0
  92. package/dist/src/test/tq-fixes.test.d.cts +17 -0
  93. package/dist/src/test/tq-fixes.test.d.ts +17 -0
  94. package/package.json +2 -2
  95. package/dist/PrismaAdapter-CvM_XNtE.cjs.map +0 -1
  96. package/dist/PrismaAdapter-Dy7MV090.js.map +0 -1
  97. package/dist/client-BAiCkZv7.js +0 -52
  98. package/dist/client-BAiCkZv7.js.map +0 -1
  99. package/dist/client-DgdG7pT6.cjs +0 -51
  100. package/dist/client-DgdG7pT6.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -45,12 +45,13 @@ var __callDispose = (stack, error, hasError) => {
45
45
  return next();
46
46
  };
47
47
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
48
- const PrismaAdapter = require("./PrismaAdapter-CvM_XNtE.cjs");
48
+ const PrismaAdapter = require("./PrismaAdapter-CUIWhjms.cjs");
49
49
  const mq = require("@supergrowthai/mq");
50
- const client = require("./client-DgdG7pT6.cjs");
50
+ const client = require("./client-BxG7LzLv.cjs");
51
51
  const utils_taskIdGen = require("./utils/task-id-gen.cjs");
52
52
  const core_Actions = require("./core/Actions.cjs");
53
- const core_async_AsyncActions = require("./core/async/AsyncActions.cjs");
53
+ const core_async_AsyncActions = require("./AsyncActions-BOO1ikWz.cjs");
54
+ const node_async_hooks = require("node:async_hooks");
54
55
  const moment = require("moment");
55
56
  const os = require("os");
56
57
  const core_async_AsyncTaskManager = require("./core/async/AsyncTaskManager.cjs");
@@ -161,7 +162,10 @@ class TaskStore {
161
162
  created_at: task.created_at || /* @__PURE__ */ new Date(),
162
163
  updated_at: task.updated_at || /* @__PURE__ */ new Date(),
163
164
  processing_started_at: task.processing_started_at || /* @__PURE__ */ new Date(),
164
- force_store: task.force_store
165
+ force_store: task.force_store,
166
+ metadata: task.metadata,
167
+ partition_key: task.partition_key,
168
+ entity: task.entity
165
169
  }));
166
170
  return await this.databaseAdapter.addTasksToScheduled(transformedTasks);
167
171
  }
@@ -669,14 +673,25 @@ class DisposableLockBatch {
669
673
  );
670
674
  }
671
675
  }
676
+ const als = new node_async_hooks.AsyncLocalStorage();
677
+ client.Logger.setContextProvider(() => als.getStore());
678
+ function runWithLogContext(store, fn) {
679
+ return als.run(store, fn);
680
+ }
681
+ function getLogContext() {
682
+ return als.getStore();
683
+ }
672
684
  class TaskRunner {
673
- constructor(messageQueue, taskQueue, taskStore, cacheProvider, generateId, lifecycleProvider, lifecycleConfig) {
685
+ constructor(messageQueue, taskQueue, taskStore, cacheProvider, generateId, lifecycleProvider, lifecycleConfig, entityProjection, entityProjectionConfig, flowMiddleware) {
674
686
  this.messageQueue = messageQueue;
675
687
  this.taskQueue = taskQueue;
676
688
  this.taskStore = taskStore;
677
689
  this.generateId = generateId;
678
690
  this.lifecycleProvider = lifecycleProvider;
679
691
  this.lifecycleConfig = lifecycleConfig;
692
+ this.entityProjection = entityProjection;
693
+ this.entityProjectionConfig = entityProjectionConfig;
694
+ this.flowMiddleware = flowMiddleware;
680
695
  this.taskStartTimes = /* @__PURE__ */ new Map();
681
696
  this.logger = new client.Logger("TaskRunner", client.LogLevel.INFO);
682
697
  this.lockManager = new LockManager(cacheProvider, {
@@ -684,6 +699,29 @@ class TaskRunner {
684
699
  defaultTimeout: 30 * 60
685
700
  });
686
701
  }
702
+ // ============ Log Context Helpers (RFC-005) ============
703
+ /**
704
+ * Build ALS log store for a single task execution.
705
+ * Runtime keys (task_id, task_type, worker_id) override user-supplied log_context.
706
+ */
707
+ buildLogStore(task, workerId) {
708
+ return {
709
+ ...task.metadata?.log_context || {},
710
+ task_id: task.id?.toString() || utils_taskIdGen.tId(task),
711
+ task_type: task.type,
712
+ worker_id: workerId
713
+ };
714
+ }
715
+ /**
716
+ * Build ALS log store for a multi-task (batch) execution.
717
+ * Only runtime keys — no user log_context (ambiguous across tasks).
718
+ */
719
+ buildBatchLogStore(tasks, workerId) {
720
+ return {
721
+ worker_id: workerId,
722
+ batch_size: String(tasks.length)
723
+ };
724
+ }
687
725
  // ============ Lifecycle Helpers ============
688
726
  async run(taskRunnerId, tasksRaw, asyncTaskManager, abortSignal) {
689
727
  var _stack = [];
@@ -692,7 +730,7 @@ class TaskRunner {
692
730
  this.logger.info(`[${taskRunnerId}] Processing ${tasksRaw.length} provided tasks`);
693
731
  if (abortSignal?.aborted) {
694
732
  this.logger.info(`[${taskRunnerId}] AbortSignal already aborted, returning empty results`);
695
- return { successTasks: [], failedTasks: [], newTasks: [], ignoredTasks: [], asyncTasks: [] };
733
+ return { successTasks: [], failedTasks: [], newTasks: [], ignoredTasks: [], asyncTasks: [], flowProjections: [] };
696
734
  }
697
735
  const tasks = await this.lockManager.filterLocked(tasksRaw, utils_taskIdGen.tId);
698
736
  this.logger.info(`[${taskRunnerId}] Found ${tasks.length} not locked tasks to process`);
@@ -713,6 +751,14 @@ class TaskRunner {
713
751
  return acc;
714
752
  }, []);
715
753
  this.logger.info(`[${taskRunnerId}] Task groups: ${groupedTasksArray.map((g) => `${g.type}: ${g.tasks.length}`).join(", ")}`);
754
+ if (this.entityProjection) {
755
+ try {
756
+ const processingProjections = tasks.filter((t) => t.entity && !t.execution_stats?.retry_count).map((t) => core_async_AsyncActions.buildProjection(t, "processing", { includePayload: this.entityProjectionConfig?.includePayload })).filter((p) => p !== null);
757
+ await core_async_AsyncActions.syncProjections(processingProjections, this.entityProjection, this.logger);
758
+ } catch (err) {
759
+ this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
760
+ }
761
+ }
716
762
  const actions = new core_Actions.Actions(taskRunnerId);
717
763
  const asyncTasks = [];
718
764
  const processedTaskIds = /* @__PURE__ */ new Set();
@@ -749,7 +795,18 @@ class TaskRunner {
749
795
  taskGroup.tasks.forEach((task) => processedTaskIds.add(utils_taskIdGen.tId(task)));
750
796
  this.logger.info(`[${taskRunnerId}] Processing ${taskGroup.tasks.length} tasks of type: ${taskGroup.type}`);
751
797
  if (executor.multiple) {
752
- await executor.onTasks(taskGroup.tasks, actions).catch((err) => this.logger.error(`[${taskRunnerId}] executor.onTasks failed: ${err}`));
798
+ const batchStore = this.buildBatchLogStore(taskGroup.tasks, taskRunnerId);
799
+ await runWithLogContext(
800
+ batchStore,
801
+ () => executor.onTasks(taskGroup.tasks, actions).catch((err) => {
802
+ this.logger.error(`[${taskRunnerId}] executor.onTasks failed: ${err}`);
803
+ for (const task of taskGroup.tasks) {
804
+ if (actions.getTaskResultStatus(utils_taskIdGen.tId(task)) === "pending") {
805
+ actions.fail(task, err instanceof Error ? err : new Error(String(err)));
806
+ }
807
+ }
808
+ })
809
+ );
753
810
  } else {
754
811
  if (executor.parallel) {
755
812
  const chunks = chunk(taskGroup.tasks, executor.chunkSize);
@@ -760,14 +817,24 @@ class TaskRunner {
760
817
  }
761
818
  const chunkPromises = [];
762
819
  for (let j = 0; j < taskChunk.length; j++) {
763
- const taskActions = actions.forkForTask(taskChunk[j]);
764
- chunkPromises.push(executor.onTask(taskChunk[j], taskActions).catch((err) => this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`)));
820
+ const task = taskChunk[j];
821
+ const taskActions = actions.forkForTask(task);
822
+ const logStore = this.buildLogStore(task, taskRunnerId);
823
+ chunkPromises.push(runWithLogContext(
824
+ logStore,
825
+ () => executor.onTask(task, taskActions).catch((err) => {
826
+ this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`);
827
+ if (actions.getTaskResultStatus(utils_taskIdGen.tId(task)) === "pending") {
828
+ actions.fail(task, err instanceof Error ? err : new Error(String(err)));
829
+ }
830
+ })
831
+ ));
765
832
  }
766
833
  await Promise.all(chunkPromises);
767
834
  for (const task of taskChunk) {
768
835
  const resultStatus = actions.getTaskResultStatus(utils_taskIdGen.tId(task));
769
836
  if (resultStatus === "success") {
770
- this.emitTaskCompleted(task, taskRunnerId);
837
+ this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(utils_taskIdGen.tId(task)));
771
838
  } else if (resultStatus === "fail") {
772
839
  const retryCount = task.execution_stats?.retry_count || 0;
773
840
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
@@ -783,10 +850,19 @@ class TaskRunner {
783
850
  if (!timeoutMs) {
784
851
  this.emitTaskStarted(task, taskRunnerId);
785
852
  const taskActions = actions.forkForTask(task);
786
- await executor.onTask(task, taskActions).catch((err) => this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`));
853
+ const logStore = this.buildLogStore(task, taskRunnerId);
854
+ await runWithLogContext(
855
+ logStore,
856
+ () => executor.onTask(task, taskActions).catch((err) => {
857
+ this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`);
858
+ if (actions.getTaskResultStatus(utils_taskIdGen.tId(task)) === "pending") {
859
+ actions.fail(task, err instanceof Error ? err : new Error(String(err)));
860
+ }
861
+ })
862
+ );
787
863
  const resultStatus = actions.getTaskResultStatus(utils_taskIdGen.tId(task));
788
864
  if (resultStatus === "success") {
789
- this.emitTaskCompleted(task, taskRunnerId);
865
+ this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(utils_taskIdGen.tId(task)));
790
866
  } else if (resultStatus === "fail") {
791
867
  const retryCount = task.execution_stats?.retry_count || 0;
792
868
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
@@ -797,9 +873,16 @@ class TaskRunner {
797
873
  this.emitTaskStarted(task, taskRunnerId);
798
874
  const startTime = Date.now();
799
875
  const taskActions = actions.forkForTask(task);
800
- const taskPromise = executor.onTask(task, taskActions).catch((err) => {
801
- this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`);
802
- });
876
+ const logStore = this.buildLogStore(task, taskRunnerId);
877
+ const taskPromise = runWithLogContext(
878
+ logStore,
879
+ () => executor.onTask(task, taskActions).catch((err) => {
880
+ this.logger.error(`[${taskRunnerId}] executor.onTask failed: ${err}`);
881
+ if (actions.getTaskResultStatus(utils_taskIdGen.tId(task)) === "pending") {
882
+ actions.fail(task, err instanceof Error ? err : new Error(String(err)));
883
+ }
884
+ })
885
+ );
803
886
  let timeoutId;
804
887
  const timeoutPromise = new Promise((resolve) => {
805
888
  timeoutId = setTimeout(() => {
@@ -813,7 +896,7 @@ class TaskRunner {
813
896
  if (result !== "~~~timeout") {
814
897
  const resultStatus = actions.getTaskResultStatus(utils_taskIdGen.tId(task));
815
898
  if (resultStatus === "success") {
816
- this.emitTaskCompleted(task, taskRunnerId);
899
+ this.emitTaskCompleted(task, taskRunnerId, actions.getTaskResult(utils_taskIdGen.tId(task)));
817
900
  } else if (resultStatus === "fail") {
818
901
  const retryCount = task.execution_stats?.retry_count || 0;
819
902
  const maxRetries = task.retries ?? executor.default_retries ?? 0;
@@ -829,7 +912,15 @@ class TaskRunner {
829
912
  if (!task.id) {
830
913
  this.logger.error(`[${taskRunnerId}] Cannot hand off task without id (type: ${task.type}). Task will continue but won't be tracked.`);
831
914
  } else {
832
- const asyncActions = new core_async_AsyncActions.AsyncActions(this.messageQueue, this.taskStore, this.taskQueue, actions, task, this.generateId);
915
+ const asyncLifecycleEmitter = this.lifecycleProvider ? {
916
+ onCompleted: (t, result2) => {
917
+ this.emitTaskCompleted(t, taskRunnerId, result2);
918
+ },
919
+ onFailed: (t, error, willRetry) => {
920
+ this.emitTaskFailed(t, taskRunnerId, error, willRetry);
921
+ }
922
+ } : void 0;
923
+ const asyncActions = new core_async_AsyncActions.AsyncActions(this.messageQueue, this.taskStore, this.taskQueue, actions, task, this.generateId, asyncLifecycleEmitter, this.entityProjection, this.entityProjectionConfig, this.flowMiddleware);
833
924
  const asyncPromise = taskPromise.finally(async () => {
834
925
  try {
835
926
  await asyncActions.onPromiseFulfilled();
@@ -888,7 +979,8 @@ class TaskRunner {
888
979
  attempt: retryCount + 1,
889
980
  max_retries: maxRetries,
890
981
  scheduled_at: task.created_at || /* @__PURE__ */ new Date(),
891
- worker_id: workerId
982
+ worker_id: workerId,
983
+ log_context: task.metadata?.log_context
892
984
  };
893
985
  }
894
986
  emitTaskStarted(task, workerId) {
@@ -985,7 +1077,10 @@ class TaskHandler {
985
1077
  this.cacheAdapter,
986
1078
  databaseAdapter.generateId.bind(databaseAdapter),
987
1079
  this.config.lifecycleProvider,
988
- this.config.lifecycle
1080
+ this.config.lifecycle,
1081
+ this.config.entityProjection,
1082
+ this.config.entityProjectionConfig,
1083
+ this.config.flowMiddleware
989
1084
  );
990
1085
  }
991
1086
  // ============ Lifecycle Event Helpers ============
@@ -995,7 +1090,38 @@ class TaskHandler {
995
1090
  get workerProvider() {
996
1091
  return this.config.workerProvider;
997
1092
  }
1093
+ get entityProjectionProvider() {
1094
+ return this.config.entityProjection;
1095
+ }
1096
+ /**
1097
+ * Validate and sanitize log_context on a task (RFC-005).
1098
+ * - >10 keys: truncate to first 10 alphabetically, warn
1099
+ * - >1024 bytes total: drop entire context, warn
1100
+ */
1101
+ validateLogContext(task) {
1102
+ const logCtx = task.metadata?.log_context;
1103
+ if (!logCtx) return task;
1104
+ let validatedCtx = logCtx;
1105
+ const keys = Object.keys(logCtx).sort();
1106
+ if (keys.length > 10) {
1107
+ this.logger.warn(`[TQ] log_context has ${keys.length} keys (max 10), truncating for task type ${task.type}`);
1108
+ const truncated = {};
1109
+ for (let i = 0; i < 10; i++) truncated[keys[i]] = logCtx[keys[i]];
1110
+ validatedCtx = truncated;
1111
+ }
1112
+ const serialized = JSON.stringify(validatedCtx);
1113
+ if (serialized.length > 1024) {
1114
+ this.logger.warn(`[TQ] log_context exceeds 1KB (${serialized.length} chars), dropping for task type ${task.type}`);
1115
+ const { log_context: _, ...restMeta } = task.metadata;
1116
+ return { ...task, metadata: Object.keys(restMeta).length > 0 ? restMeta : void 0 };
1117
+ }
1118
+ if (validatedCtx !== logCtx) {
1119
+ return { ...task, metadata: { ...task.metadata, log_context: validatedCtx } };
1120
+ }
1121
+ return task;
1122
+ }
998
1123
  async addTasks(tasks) {
1124
+ tasks = tasks.map((t) => this.validateLogContext(t));
999
1125
  const diffedItems = tasks.reduce(
1000
1126
  (acc, { force_store, ...task }) => {
1001
1127
  const currentTime = /* @__PURE__ */ new Date();
@@ -1027,8 +1153,18 @@ class TaskHandler {
1027
1153
  const executor = this.taskQueuesManager.getExecutor(task.queue_id, task.type);
1028
1154
  const shouldStoreOnFailure = executor?.store_on_failure ?? false;
1029
1155
  const id = shouldStoreOnFailure ? { id: this.databaseAdapter.generateId() } : {};
1030
- return { ...id, ...task };
1156
+ const partitionKey = executor?.getPartitionKey?.(task);
1157
+ return { ...id, ...task, ...partitionKey ? { partition_key: partitionKey } : {} };
1031
1158
  });
1159
+ if (this.entityProjectionProvider) {
1160
+ try {
1161
+ const includePayload = this.config.entityProjectionConfig?.includePayload;
1162
+ const entityProjections = queueTasks.filter((t) => t.entity).map((t) => core_async_AsyncActions.buildProjection(t, "scheduled", { includePayload })).filter((p) => p !== null);
1163
+ await core_async_AsyncActions.syncProjections(entityProjections, this.entityProjectionProvider, this.logger);
1164
+ } catch (err) {
1165
+ this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
1166
+ }
1167
+ }
1032
1168
  await this.messageQueue.addMessages(queue, queueTasks);
1033
1169
  if (this.lifecycleProvider?.onTaskScheduled) {
1034
1170
  for (const task of queueTasks) {
@@ -1047,8 +1183,18 @@ class TaskHandler {
1047
1183
  return { ...id, ...task, status: "processing", processing_started_at: /* @__PURE__ */ new Date() };
1048
1184
  });
1049
1185
  await this.taskStore.addTasksToScheduled(queueTasks);
1186
+ if (this.entityProjectionProvider) {
1187
+ try {
1188
+ const includePayload = this.config.entityProjectionConfig?.includePayload;
1189
+ const entityProjections = queueTasks.filter((t) => t.entity).map((t) => core_async_AsyncActions.buildProjection(t, "scheduled", { includePayload })).filter((p) => p !== null);
1190
+ await core_async_AsyncActions.syncProjections(entityProjections, this.entityProjectionProvider, this.logger);
1191
+ } catch (err) {
1192
+ this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
1193
+ }
1194
+ }
1195
+ const mqTasks = queueTasks.map((t) => ({ ...t, status: "scheduled" }));
1050
1196
  try {
1051
- await this.messageQueue.addMessages(queue, queueTasks);
1197
+ await this.messageQueue.addMessages(queue, mqTasks);
1052
1198
  } catch (mqError) {
1053
1199
  this.logger.error(`MQ write failed for forceStoreImmediate tasks, resetting to scheduled: ${mqError}`);
1054
1200
  const taskIds = queueTasks.map((t) => t.id).filter(Boolean);
@@ -1080,6 +1226,15 @@ class TaskHandler {
1080
1226
  return { ...id, ...task };
1081
1227
  });
1082
1228
  await this.taskStore.addTasksToScheduled(queueTasks);
1229
+ if (this.entityProjectionProvider) {
1230
+ try {
1231
+ const includePayload = this.config.entityProjectionConfig?.includePayload;
1232
+ const entityProjections = queueTasks.filter((t) => t.entity).map((t) => core_async_AsyncActions.buildProjection(t, "scheduled", { includePayload })).filter((p) => p !== null);
1233
+ await core_async_AsyncActions.syncProjections(entityProjections, this.entityProjectionProvider, this.logger);
1234
+ } catch (err) {
1235
+ this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
1236
+ }
1237
+ }
1083
1238
  if (this.lifecycleProvider?.onTaskScheduled) {
1084
1239
  for (const task of queueTasks) {
1085
1240
  this.emitLifecycleEvent(
@@ -1090,6 +1245,9 @@ class TaskHandler {
1090
1245
  }
1091
1246
  }
1092
1247
  }
1248
+ // TODO(P2): Wrap retry upsert → new tasks → mark failed → mark success in a
1249
+ // transaction. If an intermediate step fails, tasks can be lost or re-executed
1250
+ // after 2-day stale recovery. Reorder to mark success first as a quick win.
1093
1251
  async postProcessTasks({
1094
1252
  failedTasks: failedTasksRaw,
1095
1253
  newTasks,
@@ -1097,11 +1255,11 @@ class TaskHandler {
1097
1255
  }) {
1098
1256
  const tasksToRetry = [];
1099
1257
  const finalFailedTasks = [];
1100
- let discardedTasksCount = 0;
1258
+ const discardedTasks = [];
1101
1259
  const MAX_RETRY_DELAY_MS = 5 * 60 * 1e3;
1102
1260
  for (const task of failedTasksRaw) {
1103
1261
  const taskRetryCount = task.execution_stats && typeof task.execution_stats.retry_count === "number" ? task.execution_stats.retry_count : 0;
1104
- const taskRetryAfter = task.retry_after || 2e3;
1262
+ const taskRetryAfter = Math.max(task.retry_after || 2e3, 0);
1105
1263
  const calculatedDelay = taskRetryAfter * Math.pow(taskRetryCount + 1, 2);
1106
1264
  const retryAfter = Math.min(calculatedDelay, MAX_RETRY_DELAY_MS);
1107
1265
  const executeAt = Date.now() + retryAfter;
@@ -1143,7 +1301,7 @@ class TaskHandler {
1143
1301
  }]);
1144
1302
  }
1145
1303
  } else {
1146
- discardedTasksCount++;
1304
+ discardedTasks.push(task);
1147
1305
  this.logger.info(`Discarding task of type ${task.type} after ${taskRetryCount} retries`);
1148
1306
  if (this.lifecycleProvider?.onTaskExhausted) {
1149
1307
  const ctx = this.buildTaskContext(task);
@@ -1164,14 +1322,29 @@ class TaskHandler {
1164
1322
  }
1165
1323
  }
1166
1324
  }
1167
- if (discardedTasksCount > 0) {
1168
- await this.trackDiscardedTasks(discardedTasksCount);
1325
+ if (discardedTasks.length > 0) {
1326
+ await this.trackDiscardedTasks(discardedTasks.length);
1169
1327
  }
1170
1328
  if (tasksToRetry.length > 0) {
1171
- await this.taskStore.updateTasksForRetry(tasksToRetry);
1172
- }
1173
- if (newTasks.length > 0) {
1174
- await this.addTasks(newTasks);
1329
+ const dbRetryTasks = tasksToRetry.map((t) => ({
1330
+ ...t,
1331
+ status: "processing",
1332
+ processing_started_at: /* @__PURE__ */ new Date()
1333
+ }));
1334
+ await this.taskStore.updateTasksForRetry(dbRetryTasks);
1335
+ const retryByQueue = /* @__PURE__ */ new Map();
1336
+ for (const task of tasksToRetry) {
1337
+ const queue = task.queue_id;
1338
+ if (!retryByQueue.has(queue)) retryByQueue.set(queue, []);
1339
+ retryByQueue.get(queue).push(task);
1340
+ }
1341
+ for (const [queue, retryQueueTasks] of retryByQueue) {
1342
+ try {
1343
+ await this.messageQueue.addMessages(queue, retryQueueTasks);
1344
+ } catch (mqErr) {
1345
+ this.logger.error(`[TQ] Failed to re-enqueue retry tasks to MQ (stale recovery will handle): ${mqErr}`);
1346
+ }
1347
+ }
1175
1348
  }
1176
1349
  if (finalFailedTasks.length > 0) {
1177
1350
  await this.taskStore.markTasksAsFailed(finalFailedTasks);
@@ -1179,6 +1352,56 @@ class TaskHandler {
1179
1352
  if (successTasks.length > 0) {
1180
1353
  await this.taskStore.markTasksAsSuccess(successTasks);
1181
1354
  }
1355
+ if (this.entityProjectionProvider) {
1356
+ try {
1357
+ const includePayload = this.config.entityProjectionConfig?.includePayload;
1358
+ const terminalProjections = [];
1359
+ for (const task of successTasks) {
1360
+ const p = core_async_AsyncActions.buildProjection(task, "executed", {
1361
+ includePayload,
1362
+ result: task.execution_result
1363
+ });
1364
+ if (p) terminalProjections.push(p);
1365
+ }
1366
+ for (const task of finalFailedTasks) {
1367
+ const p = core_async_AsyncActions.buildProjection(task, "failed", {
1368
+ includePayload,
1369
+ error: task.execution_stats?.last_error || "Task failed"
1370
+ });
1371
+ if (p) terminalProjections.push(p);
1372
+ }
1373
+ for (const task of discardedTasks) {
1374
+ try {
1375
+ const p = core_async_AsyncActions.buildProjection(task, "failed", {
1376
+ includePayload,
1377
+ error: task.execution_stats?.last_error || "Task exhausted all retries"
1378
+ });
1379
+ if (p) terminalProjections.push(p);
1380
+ } catch (projErr) {
1381
+ this.logger.error(`[TQ] Entity projection build failed (non-fatal): ${projErr}`);
1382
+ }
1383
+ }
1384
+ await core_async_AsyncActions.syncProjections(terminalProjections, this.entityProjectionProvider, this.logger);
1385
+ } catch (err) {
1386
+ this.logger.error(`[TQ] Entity projection failed (non-fatal): ${err}`);
1387
+ }
1388
+ }
1389
+ if (this.config.flowMiddleware) {
1390
+ try {
1391
+ const flowResult = await this.config.flowMiddleware.onPostProcess({ successTasks, failedTasks: finalFailedTasks });
1392
+ if (flowResult.projections.length > 0 && this.entityProjectionProvider) {
1393
+ await core_async_AsyncActions.syncProjections(flowResult.projections, this.entityProjectionProvider, this.logger);
1394
+ }
1395
+ if (flowResult.joinTasks.length > 0) {
1396
+ await this.addTasks(flowResult.joinTasks);
1397
+ }
1398
+ } catch (err) {
1399
+ this.logger.error(`[TQ] Flow middleware failed (non-fatal): ${err}`);
1400
+ }
1401
+ }
1402
+ if (newTasks.length > 0) {
1403
+ await this.addTasks(newTasks);
1404
+ }
1182
1405
  }
1183
1406
  startConsumingTasks(streamName, abortSignal) {
1184
1407
  return this.messageQueue.consumeMessagesStream(streamName, async (id, tasks) => {
@@ -1204,10 +1427,18 @@ class TaskHandler {
1204
1427
  newTasks,
1205
1428
  successTasks,
1206
1429
  asyncTasks,
1207
- ignoredTasks
1430
+ ignoredTasks,
1431
+ flowProjections
1208
1432
  } = await this.taskRunner.run(id, tasks, this.asyncTaskManager, abortSignal).catch((err) => {
1209
- this.logger.error("Failed to execute tasks?", err);
1210
- return { failedTasks: [], newTasks: [], successTasks: [], asyncTasks: [], ignoredTasks: [] };
1433
+ this.logger.error("Failed to execute tasks, returning all as failed for retry:", err);
1434
+ return {
1435
+ failedTasks: tasks,
1436
+ newTasks: [],
1437
+ successTasks: [],
1438
+ asyncTasks: [],
1439
+ ignoredTasks: [],
1440
+ flowProjections: []
1441
+ };
1211
1442
  });
1212
1443
  if (asyncTasks.length > 0 && !this.asyncTaskManager) {
1213
1444
  throw new Error("Async tasks detected but AsyncTaskManager not initialized!");
@@ -1215,7 +1446,9 @@ class TaskHandler {
1215
1446
  if (asyncTasks.length > 0) {
1216
1447
  this.logger.info(`Handling ${asyncTasks.length} async tasks for stream ${streamName}`);
1217
1448
  for (const asyncTask of asyncTasks) {
1218
- const accepted = this.asyncTaskManager.handoffTask(asyncTask.task, asyncTask.promise);
1449
+ const executor = this.taskQueuesManager.getExecutor(asyncTask.task.queue_id, asyncTask.task.type);
1450
+ const taskTimeout = executor?.asyncConfig?.handoffTimeout ? executor.asyncConfig.handoffTimeout * 2 : void 0;
1451
+ const accepted = this.asyncTaskManager.handoffTask(asyncTask.task, asyncTask.promise, taskTimeout);
1219
1452
  if (!accepted) {
1220
1453
  this.logger.warn(`Async queue full, requeueing task ${asyncTask.task.id} with 30s delay`);
1221
1454
  await this.addTasks([{
@@ -1231,6 +1464,9 @@ class TaskHandler {
1231
1464
  this.logger.error("Failed to mark tasks as ignored", err);
1232
1465
  });
1233
1466
  }
1467
+ if (this.entityProjectionProvider && flowProjections?.length > 0) {
1468
+ await core_async_AsyncActions.syncProjections(flowProjections, this.entityProjectionProvider, this.logger);
1469
+ }
1234
1470
  await this.postProcessTasks({ failedTasks, newTasks, successTasks }).catch((err) => {
1235
1471
  this.logger.error("Failed to postProcessTasks", err);
1236
1472
  throw err;
@@ -1438,7 +1674,8 @@ class TaskHandler {
1438
1674
  payload: this.config.lifecycle?.include_payload ? payload : {},
1439
1675
  attempt: retryCount + 1,
1440
1676
  max_retries: maxRetries,
1441
- scheduled_at: task.created_at || /* @__PURE__ */ new Date()
1677
+ scheduled_at: task.created_at || /* @__PURE__ */ new Date(),
1678
+ log_context: task.metadata?.log_context
1442
1679
  };
1443
1680
  }
1444
1681
  processBatch(queueId, processor, limit, abortSignal) {
@@ -1608,6 +1845,245 @@ class TaskQueuesManager {
1608
1845
  return Array.from(queueMap.keys());
1609
1846
  }
1610
1847
  }
1848
+ class InMemoryFlowBarrierProvider {
1849
+ constructor() {
1850
+ this.barriers = /* @__PURE__ */ new Map();
1851
+ }
1852
+ async initBarrier(flowId, totalSteps) {
1853
+ if (this.barriers.has(flowId)) return;
1854
+ this.barriers.set(flowId, {
1855
+ remaining: totalSteps,
1856
+ status: "active",
1857
+ results: /* @__PURE__ */ new Map()
1858
+ });
1859
+ }
1860
+ async batchDecrementAndCheck(flowId, results) {
1861
+ const state = this.barriers.get(flowId);
1862
+ if (!state) {
1863
+ return { remaining: -1 };
1864
+ }
1865
+ if (state.status === "aborted" || state.status === "complete") {
1866
+ return { remaining: -1 };
1867
+ }
1868
+ let actualNew = 0;
1869
+ for (const result of results) {
1870
+ if (!state.results.has(result.step_index)) {
1871
+ state.results.set(result.step_index, result);
1872
+ actualNew++;
1873
+ }
1874
+ }
1875
+ state.remaining -= actualNew;
1876
+ if (state.remaining <= 0) {
1877
+ state.remaining = 0;
1878
+ state.status = "complete";
1879
+ }
1880
+ return { remaining: state.remaining };
1881
+ }
1882
+ async getStepResults(flowId) {
1883
+ const state = this.barriers.get(flowId);
1884
+ if (!state) return [];
1885
+ return Array.from(state.results.values()).sort((a, b) => a.step_index - b.step_index);
1886
+ }
1887
+ async markAborted(flowId) {
1888
+ const state = this.barriers.get(flowId);
1889
+ if (!state) return false;
1890
+ if (state.status === "aborted") {
1891
+ return false;
1892
+ }
1893
+ state.status = "aborted";
1894
+ return true;
1895
+ }
1896
+ async isComplete(flowId) {
1897
+ const state = this.barriers.get(flowId);
1898
+ if (!state) return false;
1899
+ return state.status === "complete";
1900
+ }
1901
+ }
1902
+ function getFlowMeta(task) {
1903
+ return task.metadata?.flow_meta;
1904
+ }
1905
+ function getFlowMetaRequired(task) {
1906
+ return task.metadata.flow_meta;
1907
+ }
1908
+ class FlowMiddleware {
1909
+ constructor(barrierProvider, generateId) {
1910
+ this.barrierProvider = barrierProvider;
1911
+ this.generateId = generateId;
1912
+ }
1913
+ /**
1914
+ * Process completed tasks for flow orchestration.
1915
+ * Called from TaskHandler.postProcessTasks after markFailed/markSuccess.
1916
+ *
1917
+ * @param input Categorized terminal tasks — success and final-failed (no retries left)
1918
+ * @returns Join tasks to dispatch and entity projections to sync
1919
+ */
1920
+ async onPostProcess(input) {
1921
+ const joinTasks = [];
1922
+ const projections = [];
1923
+ const successTaskSet = new Set(input.successTasks);
1924
+ const allTasks = [...input.successTasks, ...input.failedTasks];
1925
+ const joinCompletions = [];
1926
+ const timeoutTasks = [];
1927
+ const stepTasks = [];
1928
+ for (const task of allTasks) {
1929
+ const flowMeta = getFlowMeta(task);
1930
+ if (!flowMeta) continue;
1931
+ const isSuccess = successTaskSet.has(task);
1932
+ if (flowMeta.is_join) {
1933
+ joinCompletions.push(task);
1934
+ } else if (flowMeta.is_timeout) {
1935
+ timeoutTasks.push(task);
1936
+ } else {
1937
+ stepTasks.push({ task, isSuccess });
1938
+ }
1939
+ }
1940
+ for (const task of joinCompletions) {
1941
+ const flowMeta = getFlowMetaRequired(task);
1942
+ const isSuccess = successTaskSet.has(task);
1943
+ if (flowMeta.entity) {
1944
+ try {
1945
+ const status = isSuccess ? "executed" : "failed";
1946
+ const error = !isSuccess ? task.execution_stats?.last_error || "Join task failed" : void 0;
1947
+ const p = core_async_AsyncActions.buildProjection(
1948
+ {
1949
+ ...task,
1950
+ id: flowMeta.flow_id,
1951
+ entity: flowMeta.entity
1952
+ },
1953
+ status,
1954
+ { result: task.execution_result, error }
1955
+ );
1956
+ if (p) projections.push(p);
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ }
1961
+ for (const task of timeoutTasks) {
1962
+ const flowMeta = getFlowMetaRequired(task);
1963
+ const flowId = flowMeta.flow_id;
1964
+ await this.barrierProvider.initBarrier(flowId, flowMeta.total_steps);
1965
+ const isComplete = await this.barrierProvider.isComplete(flowId);
1966
+ if (isComplete) continue;
1967
+ const isFirstAbort = await this.barrierProvider.markAborted(flowId);
1968
+ if (!isFirstAbort) continue;
1969
+ const partialResults = await this.barrierProvider.getStepResults(flowId);
1970
+ const flowResults = {
1971
+ flow_id: flowId,
1972
+ steps: partialResults,
1973
+ timed_out: true
1974
+ };
1975
+ const joinTask = this.buildJoinTask(flowMeta, flowResults);
1976
+ joinTasks.push(joinTask);
1977
+ if (flowMeta.entity) {
1978
+ try {
1979
+ const p = core_async_AsyncActions.buildProjection(
1980
+ {
1981
+ ...joinTask,
1982
+ id: flowId,
1983
+ entity: flowMeta.entity
1984
+ },
1985
+ "failed",
1986
+ { error: "flow_timeout" }
1987
+ );
1988
+ if (p) projections.push(p);
1989
+ } catch {
1990
+ }
1991
+ }
1992
+ }
1993
+ const stepsByFlow = /* @__PURE__ */ new Map();
1994
+ for (const entry of stepTasks) {
1995
+ const flowMeta = getFlowMetaRequired(entry.task);
1996
+ const group = stepsByFlow.get(flowMeta.flow_id) || [];
1997
+ group.push(entry);
1998
+ stepsByFlow.set(flowMeta.flow_id, group);
1999
+ }
2000
+ for (const [flowId, entries] of stepsByFlow) {
2001
+ const firstFlowMeta = getFlowMetaRequired(entries[0].task);
2002
+ await this.barrierProvider.initBarrier(flowId, firstFlowMeta.total_steps);
2003
+ if (firstFlowMeta.failure_policy === "abort") {
2004
+ const hasFailure = entries.some((e) => !e.isSuccess);
2005
+ if (hasFailure) {
2006
+ const isFirstAbort = await this.barrierProvider.markAborted(flowId);
2007
+ if (isFirstAbort) {
2008
+ const stepResults2 = this.buildStepResults(entries);
2009
+ await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults2);
2010
+ const allResults = await this.barrierProvider.getStepResults(flowId);
2011
+ const flowResults = {
2012
+ flow_id: flowId,
2013
+ steps: allResults,
2014
+ aborted: true
2015
+ };
2016
+ const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
2017
+ joinTasks.push(joinTask);
2018
+ if (firstFlowMeta.entity) {
2019
+ try {
2020
+ const p = core_async_AsyncActions.buildProjection(
2021
+ {
2022
+ ...joinTask,
2023
+ id: flowId,
2024
+ entity: firstFlowMeta.entity
2025
+ },
2026
+ "failed",
2027
+ { error: "flow_aborted" }
2028
+ );
2029
+ if (p) projections.push(p);
2030
+ } catch {
2031
+ }
2032
+ }
2033
+ }
2034
+ continue;
2035
+ }
2036
+ }
2037
+ const stepResults = this.buildStepResults(entries);
2038
+ const { remaining } = await this.barrierProvider.batchDecrementAndCheck(flowId, stepResults);
2039
+ if (remaining === 0) {
2040
+ const allResults = await this.barrierProvider.getStepResults(flowId);
2041
+ const flowResults = {
2042
+ flow_id: flowId,
2043
+ steps: allResults
2044
+ };
2045
+ const joinTask = this.buildJoinTask(firstFlowMeta, flowResults);
2046
+ joinTasks.push(joinTask);
2047
+ }
2048
+ }
2049
+ return { joinTasks, projections };
2050
+ }
2051
+ buildStepResults(entries) {
2052
+ return entries.map(({ task, isSuccess }) => {
2053
+ const flowMeta = getFlowMetaRequired(task);
2054
+ return {
2055
+ step_index: flowMeta.step_index,
2056
+ status: isSuccess ? "success" : "fail",
2057
+ result: task.execution_result,
2058
+ error: !isSuccess ? task.execution_stats?.last_error || "Step failed" : void 0
2059
+ };
2060
+ });
2061
+ }
2062
+ buildJoinTask(flowMeta, flowResults) {
2063
+ const now = /* @__PURE__ */ new Date();
2064
+ const joinFlowMeta = {
2065
+ ...flowMeta,
2066
+ is_join: true,
2067
+ is_timeout: void 0,
2068
+ step_index: -1
2069
+ };
2070
+ return {
2071
+ id: this.generateId(),
2072
+ type: flowMeta.join.type,
2073
+ queue_id: flowMeta.join.queue_id,
2074
+ payload: { flow_results: flowResults },
2075
+ execute_at: now,
2076
+ status: "scheduled",
2077
+ created_at: now,
2078
+ updated_at: now,
2079
+ force_store: true,
2080
+ metadata: {
2081
+ flow_meta: joinFlowMeta
2082
+ },
2083
+ ...flowMeta.entity ? { entity: flowMeta.entity } : {}
2084
+ };
2085
+ }
2086
+ }
1611
2087
  class ConsoleHealthProvider {
1612
2088
  constructor(prefix = "[Health]") {
1613
2089
  this.prefix = prefix;
@@ -1670,11 +2146,17 @@ exports.PrismaAdapter = PrismaAdapter.PrismaAdapter;
1670
2146
  exports.tId = utils_taskIdGen.tId;
1671
2147
  exports.Actions = core_Actions.Actions;
1672
2148
  exports.AsyncActions = core_async_AsyncActions.AsyncActions;
2149
+ exports.buildProjection = core_async_AsyncActions.buildProjection;
2150
+ exports.syncProjections = core_async_AsyncActions.syncProjections;
1673
2151
  exports.AsyncTaskManager = core_async_AsyncTaskManager.AsyncTaskManager;
1674
2152
  exports.ConsoleHealthProvider = ConsoleHealthProvider;
2153
+ exports.FlowMiddleware = FlowMiddleware;
2154
+ exports.InMemoryFlowBarrierProvider = InMemoryFlowBarrierProvider;
1675
2155
  exports.TaskHandler = TaskHandler;
1676
2156
  exports.TaskQueuesManager = TaskQueuesManager;
1677
2157
  exports.TaskRunner = TaskRunner;
1678
2158
  exports.TaskStore = TaskStore;
1679
2159
  exports.getEnabledQueues = getEnabledQueues;
2160
+ exports.getLogContext = getLogContext;
2161
+ exports.runWithLogContext = runWithLogContext;
1680
2162
  //# sourceMappingURL=index.cjs.map