bunqueue 1.9.8 → 2.0.0

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 (219) hide show
  1. package/dist/application/backgroundTasks.d.ts +10 -0
  2. package/dist/application/backgroundTasks.d.ts.map +1 -1
  3. package/dist/application/backgroundTasks.js +64 -13
  4. package/dist/application/backgroundTasks.js.map +1 -1
  5. package/dist/application/cleanupTasks.js +5 -1
  6. package/dist/application/cleanupTasks.js.map +1 -1
  7. package/dist/application/clientTracking.d.ts.map +1 -1
  8. package/dist/application/clientTracking.js +25 -16
  9. package/dist/application/clientTracking.js.map +1 -1
  10. package/dist/application/contextFactory.d.ts.map +1 -1
  11. package/dist/application/contextFactory.js +1 -0
  12. package/dist/application/contextFactory.js.map +1 -1
  13. package/dist/application/dependencyProcessor.d.ts.map +1 -1
  14. package/dist/application/dependencyProcessor.js +14 -13
  15. package/dist/application/dependencyProcessor.js.map +1 -1
  16. package/dist/application/eventsManager.d.ts.map +1 -1
  17. package/dist/application/eventsManager.js +16 -4
  18. package/dist/application/eventsManager.js.map +1 -1
  19. package/dist/application/jobLogsManager.d.ts +2 -2
  20. package/dist/application/jobLogsManager.d.ts.map +1 -1
  21. package/dist/application/jobLogsManager.js +13 -3
  22. package/dist/application/jobLogsManager.js.map +1 -1
  23. package/dist/application/lockManager.d.ts +1 -0
  24. package/dist/application/lockManager.d.ts.map +1 -1
  25. package/dist/application/lockManager.js +54 -36
  26. package/dist/application/lockManager.js.map +1 -1
  27. package/dist/application/operations/ack.d.ts +3 -1
  28. package/dist/application/operations/ack.d.ts.map +1 -1
  29. package/dist/application/operations/ack.js +25 -1
  30. package/dist/application/operations/ack.js.map +1 -1
  31. package/dist/application/operations/ackHelpers.d.ts +1 -0
  32. package/dist/application/operations/ackHelpers.d.ts.map +1 -1
  33. package/dist/application/operations/ackHelpers.js +12 -1
  34. package/dist/application/operations/ackHelpers.js.map +1 -1
  35. package/dist/application/operations/jobManagement.d.ts +1 -1
  36. package/dist/application/operations/jobManagement.d.ts.map +1 -1
  37. package/dist/application/operations/jobManagement.js +23 -3
  38. package/dist/application/operations/jobManagement.js.map +1 -1
  39. package/dist/application/operations/push.d.ts +1 -1
  40. package/dist/application/operations/push.d.ts.map +1 -1
  41. package/dist/application/operations/push.js +25 -9
  42. package/dist/application/operations/push.js.map +1 -1
  43. package/dist/application/operations/queryOperations.d.ts +3 -0
  44. package/dist/application/operations/queryOperations.d.ts.map +1 -1
  45. package/dist/application/operations/queryOperations.js +29 -0
  46. package/dist/application/operations/queryOperations.js.map +1 -1
  47. package/dist/application/queueManager.d.ts +15 -1
  48. package/dist/application/queueManager.d.ts.map +1 -1
  49. package/dist/application/queueManager.js +77 -3
  50. package/dist/application/queueManager.js.map +1 -1
  51. package/dist/application/stallDetection.js +27 -22
  52. package/dist/application/stallDetection.js.map +1 -1
  53. package/dist/application/types.js +1 -1
  54. package/dist/application/types.js.map +1 -1
  55. package/dist/application/webhookManager.d.ts.map +1 -1
  56. package/dist/application/webhookManager.js +18 -2
  57. package/dist/application/webhookManager.js.map +1 -1
  58. package/dist/application/workerManager.d.ts.map +1 -1
  59. package/dist/application/workerManager.js +4 -2
  60. package/dist/application/workerManager.js.map +1 -1
  61. package/dist/cli/client.d.ts +3 -5
  62. package/dist/cli/client.d.ts.map +1 -1
  63. package/dist/cli/client.js +31 -27
  64. package/dist/cli/client.js.map +1 -1
  65. package/dist/cli/commands/core.js +3 -3
  66. package/dist/cli/commands/core.js.map +1 -1
  67. package/dist/cli/commands/job.js +14 -14
  68. package/dist/cli/commands/job.js.map +1 -1
  69. package/dist/cli/commands/server.d.ts.map +1 -1
  70. package/dist/cli/commands/server.js +5 -29
  71. package/dist/cli/commands/server.js.map +1 -1
  72. package/dist/cli/index.d.ts.map +1 -1
  73. package/dist/cli/index.js +1 -9
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/client/events.d.ts +29 -1
  76. package/dist/client/events.d.ts.map +1 -1
  77. package/dist/client/events.js +96 -28
  78. package/dist/client/events.js.map +1 -1
  79. package/dist/client/flow.d.ts +144 -3
  80. package/dist/client/flow.d.ts.map +1 -1
  81. package/dist/client/flow.js +538 -68
  82. package/dist/client/flow.js.map +1 -1
  83. package/dist/client/index.d.ts +2 -2
  84. package/dist/client/index.d.ts.map +1 -1
  85. package/dist/client/queue/queue.d.ts +260 -1
  86. package/dist/client/queue/queue.d.ts.map +1 -1
  87. package/dist/client/queue/queue.js +1346 -17
  88. package/dist/client/queue/queue.js.map +1 -1
  89. package/dist/client/sandboxed/types.d.ts +1 -0
  90. package/dist/client/sandboxed/types.d.ts.map +1 -1
  91. package/dist/client/sandboxed/worker.d.ts +1 -0
  92. package/dist/client/sandboxed/worker.d.ts.map +1 -1
  93. package/dist/client/sandboxed/worker.js +31 -8
  94. package/dist/client/sandboxed/worker.js.map +1 -1
  95. package/dist/client/sandboxed/wrapper.d.ts.map +1 -1
  96. package/dist/client/sandboxed/wrapper.js +10 -1
  97. package/dist/client/sandboxed/wrapper.js.map +1 -1
  98. package/dist/client/tcp/client.d.ts +4 -1
  99. package/dist/client/tcp/client.d.ts.map +1 -1
  100. package/dist/client/tcp/client.js +26 -8
  101. package/dist/client/tcp/client.js.map +1 -1
  102. package/dist/client/tcp/connection.d.ts +6 -8
  103. package/dist/client/tcp/connection.d.ts.map +1 -1
  104. package/dist/client/tcp/connection.js +24 -22
  105. package/dist/client/tcp/connection.js.map +1 -1
  106. package/dist/client/tcp/index.d.ts +0 -1
  107. package/dist/client/tcp/index.d.ts.map +1 -1
  108. package/dist/client/tcp/index.js +0 -1
  109. package/dist/client/tcp/index.js.map +1 -1
  110. package/dist/client/tcp/types.d.ts +8 -13
  111. package/dist/client/tcp/types.d.ts.map +1 -1
  112. package/dist/client/tcp/types.js +0 -1
  113. package/dist/client/tcp/types.js.map +1 -1
  114. package/dist/client/tcpPool.d.ts.map +1 -1
  115. package/dist/client/tcpPool.js +19 -14
  116. package/dist/client/tcpPool.js.map +1 -1
  117. package/dist/client/types.d.ts +430 -13
  118. package/dist/client/types.d.ts.map +1 -1
  119. package/dist/client/types.js +346 -5
  120. package/dist/client/types.js.map +1 -1
  121. package/dist/client/worker/ackBatcher.d.ts +2 -1
  122. package/dist/client/worker/ackBatcher.d.ts.map +1 -1
  123. package/dist/client/worker/ackBatcher.js +29 -18
  124. package/dist/client/worker/ackBatcher.js.map +1 -1
  125. package/dist/client/worker/jobParser.d.ts.map +1 -1
  126. package/dist/client/worker/jobParser.js +8 -7
  127. package/dist/client/worker/jobParser.js.map +1 -1
  128. package/dist/client/worker/processor.d.ts.map +1 -1
  129. package/dist/client/worker/processor.js +15 -6
  130. package/dist/client/worker/processor.js.map +1 -1
  131. package/dist/client/worker/worker.d.ts +117 -0
  132. package/dist/client/worker/worker.d.ts.map +1 -1
  133. package/dist/client/worker/worker.js +375 -3
  134. package/dist/client/worker/worker.js.map +1 -1
  135. package/dist/domain/queue/dlqShard.d.ts +2 -0
  136. package/dist/domain/queue/dlqShard.d.ts.map +1 -1
  137. package/dist/domain/queue/dlqShard.js +12 -2
  138. package/dist/domain/queue/dlqShard.js.map +1 -1
  139. package/dist/domain/queue/priorityQueue.d.ts.map +1 -1
  140. package/dist/domain/queue/priorityQueue.js +24 -18
  141. package/dist/domain/queue/priorityQueue.js.map +1 -1
  142. package/dist/domain/queue/shard.d.ts +8 -2
  143. package/dist/domain/queue/shard.d.ts.map +1 -1
  144. package/dist/domain/queue/shard.js +27 -9
  145. package/dist/domain/queue/shard.js.map +1 -1
  146. package/dist/domain/queue/temporalManager.d.ts +1 -0
  147. package/dist/domain/queue/temporalManager.d.ts.map +1 -1
  148. package/dist/domain/queue/temporalManager.js +2 -1
  149. package/dist/domain/queue/temporalManager.js.map +1 -1
  150. package/dist/domain/queue/uniqueKeyManager.d.ts +2 -2
  151. package/dist/domain/queue/uniqueKeyManager.d.ts.map +1 -1
  152. package/dist/domain/queue/uniqueKeyManager.js +3 -3
  153. package/dist/domain/queue/uniqueKeyManager.js.map +1 -1
  154. package/dist/domain/types/command.d.ts +6 -0
  155. package/dist/domain/types/command.d.ts.map +1 -1
  156. package/dist/domain/types/job.d.ts +89 -2
  157. package/dist/domain/types/job.d.ts.map +1 -1
  158. package/dist/domain/types/job.js +94 -26
  159. package/dist/domain/types/job.js.map +1 -1
  160. package/dist/domain/types/queue.d.ts +11 -1
  161. package/dist/domain/types/queue.d.ts.map +1 -1
  162. package/dist/infrastructure/persistence/sqlite.d.ts +2 -0
  163. package/dist/infrastructure/persistence/sqlite.d.ts.map +1 -1
  164. package/dist/infrastructure/persistence/sqlite.js +5 -1
  165. package/dist/infrastructure/persistence/sqlite.js.map +1 -1
  166. package/dist/infrastructure/persistence/sqliteBatch.d.ts +9 -4
  167. package/dist/infrastructure/persistence/sqliteBatch.d.ts.map +1 -1
  168. package/dist/infrastructure/persistence/sqliteBatch.js +38 -21
  169. package/dist/infrastructure/persistence/sqliteBatch.js.map +1 -1
  170. package/dist/infrastructure/persistence/sqliteSerializer.d.ts.map +1 -1
  171. package/dist/infrastructure/persistence/sqliteSerializer.js +14 -0
  172. package/dist/infrastructure/persistence/sqliteSerializer.js.map +1 -1
  173. package/dist/infrastructure/persistence/statements.d.ts +1 -1
  174. package/dist/infrastructure/persistence/statements.d.ts.map +1 -1
  175. package/dist/infrastructure/persistence/statements.js +3 -2
  176. package/dist/infrastructure/persistence/statements.js.map +1 -1
  177. package/dist/infrastructure/scheduler/cronScheduler.d.ts +7 -0
  178. package/dist/infrastructure/scheduler/cronScheduler.d.ts.map +1 -1
  179. package/dist/infrastructure/scheduler/cronScheduler.js +46 -12
  180. package/dist/infrastructure/scheduler/cronScheduler.js.map +1 -1
  181. package/dist/infrastructure/server/handlers/core.d.ts.map +1 -1
  182. package/dist/infrastructure/server/handlers/core.js +26 -19
  183. package/dist/infrastructure/server/handlers/core.js.map +1 -1
  184. package/dist/infrastructure/server/handlers/query.d.ts.map +1 -1
  185. package/dist/infrastructure/server/handlers/query.js +1 -16
  186. package/dist/infrastructure/server/handlers/query.js.map +1 -1
  187. package/dist/infrastructure/server/http.d.ts.map +1 -1
  188. package/dist/infrastructure/server/http.js +27 -2
  189. package/dist/infrastructure/server/http.js.map +1 -1
  190. package/dist/infrastructure/server/protocol.d.ts +15 -1
  191. package/dist/infrastructure/server/protocol.d.ts.map +1 -1
  192. package/dist/infrastructure/server/protocol.js +37 -3
  193. package/dist/infrastructure/server/protocol.js.map +1 -1
  194. package/dist/infrastructure/server/rateLimiter.d.ts.map +1 -1
  195. package/dist/infrastructure/server/rateLimiter.js +5 -3
  196. package/dist/infrastructure/server/rateLimiter.js.map +1 -1
  197. package/dist/infrastructure/server/tcp.d.ts +8 -10
  198. package/dist/infrastructure/server/tcp.d.ts.map +1 -1
  199. package/dist/infrastructure/server/tcp.js +87 -46
  200. package/dist/infrastructure/server/tcp.js.map +1 -1
  201. package/dist/main.js +7 -6
  202. package/dist/main.js.map +1 -1
  203. package/dist/shared/lock.d.ts +1 -1
  204. package/dist/shared/lock.d.ts.map +1 -1
  205. package/dist/shared/lock.js +6 -4
  206. package/dist/shared/lock.js.map +1 -1
  207. package/dist/shared/lru.d.ts +51 -0
  208. package/dist/shared/lru.d.ts.map +1 -1
  209. package/dist/shared/lru.js +89 -3
  210. package/dist/shared/lru.js.map +1 -1
  211. package/dist/shared/skipList.d.ts +10 -2
  212. package/dist/shared/skipList.d.ts.map +1 -1
  213. package/dist/shared/skipList.js +22 -1
  214. package/dist/shared/skipList.js.map +1 -1
  215. package/package.json +1 -1
  216. package/dist/client/tcp/lineBuffer.d.ts +0 -17
  217. package/dist/client/tcp/lineBuffer.d.ts.map +0 -1
  218. package/dist/client/tcp/lineBuffer.js +0 -32
  219. package/dist/client/tcp/lineBuffer.js.map +0 -1
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * FlowProducer - Job chaining and pipelines
3
+ * BullMQ v5 compatible
3
4
  */
4
5
  import { getSharedManager } from './manager';
6
+ import { TcpConnectionPool, getSharedPool, releaseSharedPool } from './tcpPool';
5
7
  import { jobId } from '../domain/types/job';
8
+ const FORCE_EMBEDDED = process.env.BUNQUEUE_EMBEDDED === '1';
6
9
  /**
7
10
  * FlowProducer creates job flows with automatic dependencies.
8
11
  *
@@ -28,6 +31,453 @@ import { jobId } from '../domain/types/job';
28
31
  * ```
29
32
  */
30
33
  export class FlowProducer {
34
+ embedded;
35
+ tcp;
36
+ useSharedPool;
37
+ constructor(opts = {}) {
38
+ this.embedded = opts.embedded ?? FORCE_EMBEDDED;
39
+ if (this.embedded) {
40
+ this.tcp = null;
41
+ this.useSharedPool = false;
42
+ }
43
+ else {
44
+ const connOpts = opts.connection ?? {};
45
+ const poolSize = connOpts.poolSize ?? 4;
46
+ if (poolSize === 4 && !connOpts.token) {
47
+ this.tcp = getSharedPool({ host: connOpts.host, port: connOpts.port, poolSize });
48
+ this.useSharedPool = true;
49
+ }
50
+ else {
51
+ this.tcp = new TcpConnectionPool({
52
+ host: connOpts.host ?? 'localhost',
53
+ port: connOpts.port ?? 6789,
54
+ token: connOpts.token,
55
+ poolSize,
56
+ });
57
+ this.useSharedPool = false;
58
+ }
59
+ }
60
+ }
61
+ /** Close the connection pool (only if using dedicated pool) */
62
+ close() {
63
+ if (this.tcp && !this.useSharedPool) {
64
+ this.tcp.close();
65
+ }
66
+ else if (this.tcp && this.useSharedPool) {
67
+ releaseSharedPool(this.tcp);
68
+ }
69
+ }
70
+ /**
71
+ * Disconnect from the server (BullMQ v5 compatible).
72
+ * Alias for close().
73
+ */
74
+ disconnect() {
75
+ this.close();
76
+ return Promise.resolve();
77
+ }
78
+ /**
79
+ * Wait until the FlowProducer is ready (BullMQ v5 compatible).
80
+ * In embedded mode, resolves immediately. In TCP mode, ensures connection.
81
+ */
82
+ async waitUntilReady() {
83
+ if (this.embedded) {
84
+ // Embedded mode is always ready
85
+ return;
86
+ }
87
+ // TCP mode - ensure we can send a ping
88
+ if (this.tcp) {
89
+ await this.tcp.send({ cmd: 'Ping' });
90
+ }
91
+ }
92
+ // ============================================================================
93
+ // BullMQ v5 Compatible Methods
94
+ // ============================================================================
95
+ /**
96
+ * Add a flow (BullMQ v5 compatible).
97
+ *
98
+ * Children are processed BEFORE their parent. When all children complete,
99
+ * the parent becomes processable and can access children results via
100
+ * `job.getChildrenValues()`.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const flow = new FlowProducer();
105
+ *
106
+ * // Children execute first, then parent
107
+ * const { job, children } = await flow.add({
108
+ * name: 'parent',
109
+ * queueName: 'main',
110
+ * data: { type: 'aggregate' },
111
+ * children: [
112
+ * { name: 'child1', queueName: 'main', data: { id: 1 } },
113
+ * { name: 'child2', queueName: 'main', data: { id: 2 } },
114
+ * ],
115
+ * });
116
+ * ```
117
+ */
118
+ async add(flow) {
119
+ return this.addFlowNode(flow, null);
120
+ }
121
+ /**
122
+ * Add multiple flows (BullMQ v5 compatible).
123
+ */
124
+ async addBulk(flows) {
125
+ const results = [];
126
+ for (const flow of flows) {
127
+ results.push(await this.add(flow));
128
+ }
129
+ return results;
130
+ }
131
+ /**
132
+ * Get a flow tree starting from a job (BullMQ v5 compatible).
133
+ *
134
+ * Retrieves the job and all its children recursively, building a JobNode tree.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const flow = new FlowProducer();
139
+ *
140
+ * // Get a previously created flow
141
+ * const node = await flow.getFlow({
142
+ * id: 'job-id',
143
+ * queueName: 'my-queue',
144
+ * });
145
+ *
146
+ * if (node) {
147
+ * console.log('Parent:', node.job.name);
148
+ * console.log('Children:', node.children?.length ?? 0);
149
+ * }
150
+ * ```
151
+ */
152
+ async getFlow(opts) {
153
+ const { id, queueName, depth, maxChildren } = opts;
154
+ if (this.embedded) {
155
+ return this.getFlowEmbedded(id, queueName, depth ?? Infinity, maxChildren);
156
+ }
157
+ return this.getFlowTcp(id, queueName, depth ?? Infinity, maxChildren);
158
+ }
159
+ /** Get flow in embedded mode */
160
+ async getFlowEmbedded(id, queueName, depth, maxChildren) {
161
+ const manager = getSharedManager();
162
+ const job = await manager.getJob(jobId(id));
163
+ if (job?.queue !== queueName) {
164
+ return null;
165
+ }
166
+ return this.buildJobNode(job, depth, maxChildren);
167
+ }
168
+ /** Get flow in TCP mode */
169
+ async getFlowTcp(id, queueName, depth, maxChildren) {
170
+ if (!this.tcp)
171
+ throw new Error('TCP connection not initialized');
172
+ const response = await this.tcp.send({
173
+ cmd: 'GetJob',
174
+ id,
175
+ });
176
+ if (!response.ok || !response.job) {
177
+ return null;
178
+ }
179
+ const jobData = response.job;
180
+ if (jobData.queue !== queueName) {
181
+ return null;
182
+ }
183
+ // Build job node from TCP response
184
+ return this.buildJobNodeFromTcp(jobData, depth, maxChildren);
185
+ }
186
+ /** Build JobNode recursively from internal job */
187
+ async buildJobNode(job, depth, maxChildren) {
188
+ const data = job.data;
189
+ const nameValue = data.name;
190
+ const name = typeof nameValue === 'string' ? nameValue : 'default';
191
+ const userData = this.extractUserDataFromInternal(data);
192
+ const jobObj = this.createJobObject(String(job.id), name, userData, job.queue);
193
+ // If depth exhausted or no children, return without children
194
+ if (depth <= 0 || job.childrenIds.length === 0) {
195
+ return { job: jobObj };
196
+ }
197
+ // Fetch children recursively
198
+ const manager = getSharedManager();
199
+ const childNodes = [];
200
+ const childrenToFetch = maxChildren ? job.childrenIds.slice(0, maxChildren) : job.childrenIds;
201
+ for (const childId of childrenToFetch) {
202
+ const childJob = await manager.getJob(childId);
203
+ if (childJob) {
204
+ const childNode = await this.buildJobNode(childJob, depth - 1, maxChildren);
205
+ childNodes.push(childNode);
206
+ }
207
+ }
208
+ return {
209
+ job: jobObj,
210
+ children: childNodes.length > 0 ? childNodes : undefined,
211
+ };
212
+ }
213
+ /** Build JobNode from TCP response */
214
+ async buildJobNodeFromTcp(jobData, depth, maxChildren) {
215
+ const id = String(jobData.id);
216
+ const queueName = String(jobData.queue);
217
+ const data = jobData.data;
218
+ const nameValue = data?.name;
219
+ const name = typeof nameValue === 'string' ? nameValue : 'default';
220
+ const userData = this.extractUserDataFromInternal(data ?? {});
221
+ const rawChildrenIds = data?.__childrenIds;
222
+ const childrenIds = Array.isArray(rawChildrenIds) ? rawChildrenIds : [];
223
+ const jobObj = this.createJobObject(id, name, userData, queueName);
224
+ // If depth exhausted or no children, return without children
225
+ if (depth <= 0 || childrenIds.length === 0) {
226
+ return { job: jobObj };
227
+ }
228
+ // Fetch children recursively via TCP
229
+ if (!this.tcp) {
230
+ return { job: jobObj };
231
+ }
232
+ const childNodes = [];
233
+ const childrenToFetch = maxChildren ? childrenIds.slice(0, maxChildren) : childrenIds;
234
+ for (const childId of childrenToFetch) {
235
+ const response = await this.tcp.send({ cmd: 'GetJob', id: childId });
236
+ if (response.ok && response.job) {
237
+ const childNode = await this.buildJobNodeFromTcp(response.job, depth - 1, maxChildren);
238
+ childNodes.push(childNode);
239
+ }
240
+ }
241
+ return {
242
+ job: jobObj,
243
+ children: childNodes.length > 0 ? childNodes : undefined,
244
+ };
245
+ }
246
+ /** Extract user data (remove internal fields) */
247
+ extractUserDataFromInternal(data) {
248
+ const result = {};
249
+ for (const [key, value] of Object.entries(data)) {
250
+ if (!key.startsWith('__') && key !== 'name') {
251
+ result[key] = value;
252
+ }
253
+ }
254
+ return result;
255
+ }
256
+ /**
257
+ * Internal: Recursively add a flow node and its children.
258
+ * Children are created first with parent reference, then parent is created.
259
+ */
260
+ async addFlowNode(node, parentRef) {
261
+ // First, create all children recursively
262
+ const childNodes = [];
263
+ const childIds = [];
264
+ if (node.children && node.children.length > 0) {
265
+ // Create a placeholder parent ID for children to reference
266
+ // In BullMQ, children reference the parent, not the other way around
267
+ const tempParentRef = { id: 'pending', queue: node.queueName };
268
+ for (const child of node.children) {
269
+ const childNode = await this.addFlowNode(child, tempParentRef);
270
+ childNodes.push(childNode);
271
+ childIds.push(childNode.job.id);
272
+ }
273
+ }
274
+ // Create the job data with parent info if this is a child
275
+ const jobData = {
276
+ name: node.name,
277
+ ...node.data,
278
+ };
279
+ // Add parent reference if this job has a parent
280
+ if (parentRef) {
281
+ jobData.__parentId = parentRef.id;
282
+ jobData.__parentQueue = parentRef.queue;
283
+ }
284
+ // Add children IDs so parent knows its children
285
+ if (childIds.length > 0) {
286
+ jobData.__childrenIds = childIds;
287
+ }
288
+ // Push the job
289
+ const jobIdStr = await this.pushJobWithParent(node.queueName, jobData, node.opts ?? {}, parentRef, childIds);
290
+ // Create the Job object
291
+ const job = this.createJobObject(jobIdStr, node.name, node.data, node.queueName);
292
+ return {
293
+ job,
294
+ children: childNodes.length > 0 ? childNodes : undefined,
295
+ };
296
+ }
297
+ /** Push a job with parent/children tracking */
298
+ async pushJobWithParent(queueName, data, opts, parentRef, childIds) {
299
+ if (this.embedded) {
300
+ const manager = getSharedManager();
301
+ // Parse removeOnComplete/removeOnFail (can be boolean, number, or KeepJobs)
302
+ const removeOnComplete = typeof opts.removeOnComplete === 'boolean' ? opts.removeOnComplete : false;
303
+ const removeOnFail = typeof opts.removeOnFail === 'boolean' ? opts.removeOnFail : false;
304
+ const job = await manager.push(queueName, {
305
+ data,
306
+ priority: opts.priority,
307
+ delay: opts.delay,
308
+ maxAttempts: opts.attempts,
309
+ backoff: opts.backoff,
310
+ timeout: opts.timeout,
311
+ customId: opts.jobId,
312
+ removeOnComplete,
313
+ removeOnFail,
314
+ parentId: parentRef ? jobId(parentRef.id) : undefined,
315
+ // Note: childrenIds tracking happens via job.childrenIds field
316
+ });
317
+ // If this job has children, update the children's parent references
318
+ // with the actual parent ID (they were created with 'pending')
319
+ if (childIds.length > 0) {
320
+ for (const childIdStr of childIds) {
321
+ await manager.updateJobParent(jobId(childIdStr), job.id);
322
+ }
323
+ }
324
+ return String(job.id);
325
+ }
326
+ // TCP mode
327
+ if (!this.tcp)
328
+ throw new Error('TCP connection not initialized');
329
+ const response = await this.tcp.send({
330
+ cmd: 'PUSH',
331
+ queue: queueName,
332
+ data,
333
+ priority: opts.priority,
334
+ delay: opts.delay,
335
+ maxAttempts: opts.attempts,
336
+ backoff: opts.backoff,
337
+ timeout: opts.timeout,
338
+ jobId: opts.jobId,
339
+ removeOnComplete: opts.removeOnComplete,
340
+ removeOnFail: opts.removeOnFail,
341
+ parentId: parentRef?.id,
342
+ childIds,
343
+ });
344
+ if (!response.ok) {
345
+ throw new Error(response.error ?? 'Failed to add job');
346
+ }
347
+ return response.id;
348
+ }
349
+ /** Create a simple Job object */
350
+ createJobObject(id, name, data, queueName) {
351
+ const ts = Date.now();
352
+ return {
353
+ id,
354
+ name,
355
+ data,
356
+ queueName,
357
+ attemptsMade: 0,
358
+ timestamp: ts,
359
+ progress: 0,
360
+ // BullMQ v5 properties
361
+ delay: 0,
362
+ processedOn: undefined,
363
+ finishedOn: undefined,
364
+ stacktrace: null,
365
+ stalledCounter: 0,
366
+ priority: 0,
367
+ parentKey: undefined,
368
+ opts: {},
369
+ token: undefined,
370
+ processedBy: undefined,
371
+ deduplicationId: undefined,
372
+ repeatJobKey: undefined,
373
+ attemptsStarted: 0,
374
+ // Methods
375
+ updateProgress: () => Promise.resolve(),
376
+ log: () => Promise.resolve(),
377
+ getState: () => Promise.resolve('waiting'),
378
+ remove: () => Promise.resolve(),
379
+ retry: () => Promise.resolve(),
380
+ getChildrenValues: () => Promise.resolve({}),
381
+ // BullMQ v5 state check methods
382
+ isWaiting: () => Promise.resolve(true),
383
+ isActive: () => Promise.resolve(false),
384
+ isDelayed: () => Promise.resolve(false),
385
+ isCompleted: () => Promise.resolve(false),
386
+ isFailed: () => Promise.resolve(false),
387
+ isWaitingChildren: () => Promise.resolve(false),
388
+ // BullMQ v5 mutation methods
389
+ updateData: () => Promise.resolve(),
390
+ promote: () => Promise.resolve(),
391
+ changeDelay: () => Promise.resolve(),
392
+ changePriority: () => Promise.resolve(),
393
+ extendLock: () => Promise.resolve(0),
394
+ clearLogs: () => Promise.resolve(),
395
+ // BullMQ v5 dependency methods
396
+ getDependencies: () => Promise.resolve({ processed: {}, unprocessed: [] }),
397
+ getDependenciesCount: () => Promise.resolve({ processed: 0, unprocessed: 0 }),
398
+ // BullMQ v5 serialization methods
399
+ toJSON: () => ({
400
+ id,
401
+ name,
402
+ data,
403
+ opts: {},
404
+ progress: 0,
405
+ delay: 0,
406
+ timestamp: ts,
407
+ attemptsMade: 0,
408
+ stacktrace: null,
409
+ queueQualifiedName: `bull:${queueName}`,
410
+ }),
411
+ asJSON: () => ({
412
+ id,
413
+ name,
414
+ data: JSON.stringify(data),
415
+ opts: '{}',
416
+ progress: '0',
417
+ delay: '0',
418
+ timestamp: String(ts),
419
+ attemptsMade: '0',
420
+ stacktrace: null,
421
+ }),
422
+ // BullMQ v5 move methods
423
+ moveToCompleted: () => Promise.resolve(null),
424
+ moveToFailed: () => Promise.resolve(),
425
+ moveToWait: () => Promise.resolve(false),
426
+ moveToDelayed: () => Promise.resolve(),
427
+ moveToWaitingChildren: () => Promise.resolve(false),
428
+ waitUntilFinished: () => Promise.resolve(undefined),
429
+ // BullMQ v5 additional methods
430
+ discard: () => { },
431
+ getFailedChildrenValues: () => Promise.resolve({}),
432
+ getIgnoredChildrenFailures: () => Promise.resolve({}),
433
+ removeChildDependency: () => Promise.resolve(false),
434
+ removeDeduplicationKey: () => Promise.resolve(false),
435
+ removeUnprocessedChildren: () => Promise.resolve(),
436
+ };
437
+ }
438
+ /** Push a job via embedded manager or TCP */
439
+ async pushJob(queueName, data, opts = {}, dependsOn) {
440
+ if (this.embedded) {
441
+ const manager = getSharedManager();
442
+ // Parse removeOnComplete/removeOnFail (can be boolean, number, or KeepJobs)
443
+ const removeOnComplete = typeof opts.removeOnComplete === 'boolean' ? opts.removeOnComplete : false;
444
+ const removeOnFail = typeof opts.removeOnFail === 'boolean' ? opts.removeOnFail : false;
445
+ const job = await manager.push(queueName, {
446
+ data,
447
+ priority: opts.priority,
448
+ delay: opts.delay,
449
+ maxAttempts: opts.attempts,
450
+ backoff: opts.backoff,
451
+ timeout: opts.timeout,
452
+ customId: opts.jobId,
453
+ removeOnComplete,
454
+ removeOnFail,
455
+ dependsOn: dependsOn?.map((id) => jobId(id)),
456
+ });
457
+ return String(job.id);
458
+ }
459
+ // TCP mode - tcp is guaranteed to exist when not embedded
460
+ if (!this.tcp)
461
+ throw new Error('TCP connection not initialized');
462
+ const response = await this.tcp.send({
463
+ cmd: 'PUSH',
464
+ queue: queueName,
465
+ data,
466
+ priority: opts.priority,
467
+ delay: opts.delay,
468
+ maxAttempts: opts.attempts,
469
+ backoff: opts.backoff,
470
+ timeout: opts.timeout,
471
+ jobId: opts.jobId,
472
+ removeOnComplete: opts.removeOnComplete,
473
+ removeOnFail: opts.removeOnFail,
474
+ dependsOn,
475
+ });
476
+ if (!response.ok) {
477
+ throw new Error(response.error ?? 'Failed to add job');
478
+ }
479
+ return response.id;
480
+ }
31
481
  /**
32
482
  * Add a chain of jobs where each depends on the previous.
33
483
  * Jobs execute sequentially: step[0] → step[1] → step[2] → ...
@@ -36,27 +486,21 @@ export class FlowProducer {
36
486
  if (steps.length === 0) {
37
487
  return { jobIds: [] };
38
488
  }
39
- const manager = getSharedManager();
40
489
  const jobIds = [];
41
490
  let prevId = null;
42
- for (const step of steps) {
43
- const merged = step.opts ?? {};
44
- const input = {
45
- data: { name: step.name, __flowParentId: prevId, ...step.data },
46
- priority: merged.priority,
47
- delay: merged.delay,
48
- maxAttempts: merged.attempts,
49
- backoff: merged.backoff,
50
- timeout: merged.timeout,
51
- customId: merged.jobId,
52
- removeOnComplete: merged.removeOnComplete,
53
- removeOnFail: merged.removeOnFail,
54
- dependsOn: prevId ? [jobId(prevId)] : undefined,
55
- };
56
- const job = await manager.push(step.queueName, input);
57
- const id = String(job.id);
58
- jobIds.push(id);
59
- prevId = id;
491
+ try {
492
+ for (const step of steps) {
493
+ const merged = step.opts ?? {};
494
+ const data = { name: step.name, __flowParentId: prevId, ...step.data };
495
+ const id = await this.pushJob(step.queueName, data, merged, prevId ? [prevId] : undefined);
496
+ jobIds.push(id);
497
+ prevId = id;
498
+ }
499
+ }
500
+ catch (error) {
501
+ // Cleanup already-created jobs on failure
502
+ await this.cleanupJobs(jobIds);
503
+ throw error;
60
504
  }
61
505
  return { jobIds };
62
506
  }
@@ -72,42 +516,33 @@ export class FlowProducer {
72
516
  * ```
73
517
  */
74
518
  async addBulkThen(parallel, final) {
75
- const manager = getSharedManager();
76
519
  // Create parallel jobs (no dependencies)
77
520
  const parallelIds = [];
78
- for (const step of parallel) {
79
- const merged = step.opts ?? {};
80
- const job = await manager.push(step.queueName, {
81
- data: { name: step.name, ...step.data },
82
- priority: merged.priority,
83
- delay: merged.delay,
84
- maxAttempts: merged.attempts,
85
- backoff: merged.backoff,
86
- timeout: merged.timeout,
87
- customId: merged.jobId,
88
- removeOnComplete: merged.removeOnComplete,
89
- removeOnFail: merged.removeOnFail,
90
- });
91
- parallelIds.push(String(job.id));
92
- }
93
- // Create final job with dependencies on all parallel jobs
94
- const finalMerged = final.opts ?? {};
95
- const finalJob = await manager.push(final.queueName, {
96
- data: { name: final.name, __flowParentIds: parallelIds, ...final.data },
97
- priority: finalMerged.priority,
98
- delay: finalMerged.delay,
99
- maxAttempts: finalMerged.attempts,
100
- backoff: finalMerged.backoff,
101
- timeout: finalMerged.timeout,
102
- customId: finalMerged.jobId,
103
- removeOnComplete: finalMerged.removeOnComplete,
104
- removeOnFail: finalMerged.removeOnFail,
105
- dependsOn: parallelIds.map((id) => jobId(id)),
106
- });
107
- return {
108
- parallelIds,
109
- finalId: String(finalJob.id),
110
- };
521
+ try {
522
+ for (const step of parallel) {
523
+ const merged = step.opts ?? {};
524
+ const data = { name: step.name, ...step.data };
525
+ const id = await this.pushJob(step.queueName, data, merged);
526
+ parallelIds.push(id);
527
+ }
528
+ // Create final job with dependencies on all parallel jobs
529
+ const finalMerged = final.opts ?? {};
530
+ const finalData = {
531
+ name: final.name,
532
+ __flowParentIds: parallelIds,
533
+ ...final.data,
534
+ };
535
+ const finalId = await this.pushJob(final.queueName, finalData, finalMerged, parallelIds);
536
+ return {
537
+ parallelIds,
538
+ finalId,
539
+ };
540
+ }
541
+ catch (error) {
542
+ // Cleanup already-created parallel jobs on failure
543
+ await this.cleanupJobs(parallelIds);
544
+ throw error;
545
+ }
111
546
  }
112
547
  /**
113
548
  * Add a tree of jobs where children depend on parent.
@@ -115,25 +550,20 @@ export class FlowProducer {
115
550
  */
116
551
  async addTree(root) {
117
552
  const jobIds = [];
118
- await this.addTreeNode(root, null, jobIds);
553
+ try {
554
+ await this.addTreeNode(root, null, jobIds);
555
+ }
556
+ catch (error) {
557
+ // Cleanup already-created jobs on failure
558
+ await this.cleanupJobs(jobIds);
559
+ throw error;
560
+ }
119
561
  return { jobIds };
120
562
  }
121
563
  async addTreeNode(step, parentId, jobIds) {
122
- const manager = getSharedManager();
123
564
  const merged = step.opts ?? {};
124
- const job = await manager.push(step.queueName, {
125
- data: { name: step.name, __flowParentId: parentId, ...step.data },
126
- priority: merged.priority,
127
- delay: merged.delay,
128
- maxAttempts: merged.attempts,
129
- backoff: merged.backoff,
130
- timeout: merged.timeout,
131
- customId: merged.jobId,
132
- removeOnComplete: merged.removeOnComplete,
133
- removeOnFail: merged.removeOnFail,
134
- dependsOn: parentId ? [jobId(parentId)] : undefined,
135
- });
136
- const id = String(job.id);
565
+ const data = { name: step.name, __flowParentId: parentId, ...step.data };
566
+ const id = await this.pushJob(step.queueName, data, merged, parentId ? [parentId] : undefined);
137
567
  jobIds.push(id);
138
568
  // Create children with this job as parent
139
569
  if (step.children) {
@@ -143,18 +573,58 @@ export class FlowProducer {
143
573
  }
144
574
  return id;
145
575
  }
576
+ /**
577
+ * Cleanup jobs that were created before a failure occurred.
578
+ * Cancels each job to prevent orphaned jobs in the queue.
579
+ */
580
+ async cleanupJobs(jobIds) {
581
+ if (jobIds.length === 0)
582
+ return;
583
+ if (this.embedded) {
584
+ const manager = getSharedManager();
585
+ const cleanupPromises = jobIds.map(async (id) => {
586
+ try {
587
+ await manager.cancel(jobId(id));
588
+ }
589
+ catch {
590
+ // Ignore errors during cleanup - job may already be processed or removed
591
+ }
592
+ });
593
+ await Promise.all(cleanupPromises);
594
+ }
595
+ else if (this.tcp) {
596
+ // TCP mode cleanup
597
+ const cleanupPromises = jobIds.map(async (id) => {
598
+ try {
599
+ await this.tcp?.send({ cmd: 'Cancel', id });
600
+ }
601
+ catch {
602
+ // Ignore errors during cleanup
603
+ }
604
+ });
605
+ await Promise.all(cleanupPromises);
606
+ }
607
+ }
146
608
  /**
147
609
  * Get the result of a completed parent job.
148
610
  * Call this from within a worker to access the previous step's result.
611
+ * Note: Only works in embedded mode.
149
612
  */
150
613
  getParentResult(parentId) {
614
+ if (!this.embedded) {
615
+ throw new Error('getParentResult is only available in embedded mode');
616
+ }
151
617
  const manager = getSharedManager();
152
618
  return manager.getResult(jobId(parentId));
153
619
  }
154
620
  /**
155
621
  * Get results from multiple parent jobs (for merge scenarios).
622
+ * Note: Only works in embedded mode.
156
623
  */
157
624
  getParentResults(parentIds) {
625
+ if (!this.embedded) {
626
+ throw new Error('getParentResults is only available in embedded mode');
627
+ }
158
628
  const manager = getSharedManager();
159
629
  const results = new Map();
160
630
  for (const id of parentIds) {