bunqueue 2.8.49 → 2.8.51

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 (149) hide show
  1. package/README.md +62 -9
  2. package/dist/application/backgroundTasks.js +21 -4
  3. package/dist/application/cleanupTasks.js +7 -3
  4. package/dist/application/contextFactory.d.ts +6 -3
  5. package/dist/application/contextFactory.js +8 -0
  6. package/dist/application/dependencyCompletions.d.ts +53 -0
  7. package/dist/application/dependencyCompletions.js +123 -0
  8. package/dist/application/dependencyProcessor.d.ts +5 -0
  9. package/dist/application/dependencyProcessor.js +25 -13
  10. package/dist/application/flowFailureRecovery.d.ts +20 -0
  11. package/dist/application/flowFailureRecovery.js +94 -0
  12. package/dist/application/flowParentBackpatch.d.ts +27 -0
  13. package/dist/application/flowParentBackpatch.js +120 -0
  14. package/dist/application/operations/ack.d.ts +6 -3
  15. package/dist/application/operations/ack.js +36 -10
  16. package/dist/application/operations/ackHelpers.d.ts +5 -4
  17. package/dist/application/operations/ackHelpers.js +4 -10
  18. package/dist/application/operations/customId.d.ts +3 -0
  19. package/dist/application/operations/customId.js +10 -0
  20. package/dist/application/operations/flowPush.d.ts +9 -0
  21. package/dist/application/operations/flowPush.js +112 -0
  22. package/dist/application/operations/flowTopologyValidation.d.ts +3 -0
  23. package/dist/application/operations/flowTopologyValidation.js +101 -0
  24. package/dist/application/operations/flowValidation.d.ts +3 -0
  25. package/dist/application/operations/flowValidation.js +166 -0
  26. package/dist/application/operations/jobManagement.d.ts +3 -0
  27. package/dist/application/operations/jobManagement.js +10 -4
  28. package/dist/application/operations/push.d.ts +6 -1
  29. package/dist/application/operations/push.js +2 -2
  30. package/dist/application/operations/pushInsert.d.ts +11 -2
  31. package/dist/application/operations/pushInsert.js +20 -8
  32. package/dist/application/operations/pushLocks.d.ts +1 -0
  33. package/dist/application/operations/pushLocks.js +5 -1
  34. package/dist/application/operations/queryOperations.js +7 -6
  35. package/dist/application/queueManager.d.ts +7 -0
  36. package/dist/application/queueManager.js +261 -67
  37. package/dist/application/types.d.ts +3 -1
  38. package/dist/cli/commandRouter.d.ts +1 -1
  39. package/dist/client/flow.d.ts +3 -6
  40. package/dist/client/flow.js +71 -210
  41. package/dist/client/flowAtomic.d.ts +9 -0
  42. package/dist/client/flowAtomic.js +24 -0
  43. package/dist/client/flowJobCoreMethods.d.ts +28 -0
  44. package/dist/client/flowJobCoreMethods.js +151 -0
  45. package/dist/client/flowJobDependencies.d.ts +7 -0
  46. package/dist/client/flowJobDependencies.js +64 -0
  47. package/dist/client/flowJobFactory.d.ts +11 -24
  48. package/dist/client/flowJobFactory.js +91 -360
  49. package/dist/client/flowJobMoveMethods.d.ts +16 -0
  50. package/dist/client/flowJobMoveMethods.js +112 -0
  51. package/dist/client/flowJobTypes.d.ts +29 -0
  52. package/dist/client/flowJobTypes.js +6 -0
  53. package/dist/client/flowLegacyPlan.d.ts +16 -0
  54. package/dist/client/flowLegacyPlan.js +118 -0
  55. package/dist/client/flowOptions.d.ts +10 -0
  56. package/dist/client/flowOptions.js +44 -0
  57. package/dist/client/flowPlan.d.ts +16 -0
  58. package/dist/client/flowPlan.js +101 -0
  59. package/dist/client/flowPush.js +4 -1
  60. package/dist/client/flowReader.d.ts +10 -0
  61. package/dist/client/flowReader.js +147 -0
  62. package/dist/client/flowTypes.d.ts +2 -2
  63. package/dist/client/jobHelpers.js +2 -0
  64. package/dist/client/workflow/clock.d.ts +3 -1
  65. package/dist/client/workflow/clock.js +18 -1
  66. package/dist/client/workflow/compensationChild.d.ts +30 -0
  67. package/dist/client/workflow/compensationChild.js +65 -0
  68. package/dist/client/workflow/compensationPass.d.ts +16 -0
  69. package/dist/client/workflow/compensationPass.js +96 -0
  70. package/dist/client/workflow/compensationSupport.d.ts +24 -0
  71. package/dist/client/workflow/compensationSupport.js +57 -0
  72. package/dist/client/workflow/compensator.d.ts +11 -50
  73. package/dist/client/workflow/compensator.js +22 -374
  74. package/dist/client/workflow/definitionGuard.d.ts +11 -0
  75. package/dist/client/workflow/definitionGuard.js +22 -0
  76. package/dist/client/workflow/engine.d.ts +2 -2
  77. package/dist/client/workflow/engine.js +8 -8
  78. package/dist/client/workflow/eventTypes.d.ts +44 -0
  79. package/dist/client/workflow/eventTypes.js +2 -0
  80. package/dist/client/workflow/executionTypes.d.ts +133 -0
  81. package/dist/client/workflow/executionTypes.js +2 -0
  82. package/dist/client/workflow/executor.d.ts +4 -17
  83. package/dist/client/workflow/executor.js +50 -208
  84. package/dist/client/workflow/executorLifecycle.d.ts +18 -0
  85. package/dist/client/workflow/executorLifecycle.js +69 -0
  86. package/dist/client/workflow/executorNodes.d.ts +17 -0
  87. package/dist/client/workflow/executorNodes.js +138 -0
  88. package/dist/client/workflow/identity.d.ts +2 -0
  89. package/dist/client/workflow/identity.js +8 -0
  90. package/dist/client/workflow/index.d.ts +1 -1
  91. package/dist/client/workflow/loops.d.ts +3 -5
  92. package/dist/client/workflow/loops.js +25 -23
  93. package/dist/client/workflow/mapRunner.d.ts +4 -0
  94. package/dist/client/workflow/mapRunner.js +45 -0
  95. package/dist/client/workflow/recovery.js +10 -3
  96. package/dist/client/workflow/runner.d.ts +2 -21
  97. package/dist/client/workflow/runner.js +39 -104
  98. package/dist/client/workflow/runnerTiming.d.ts +14 -0
  99. package/dist/client/workflow/runnerTiming.js +66 -0
  100. package/dist/client/workflow/stepTypes.d.ts +171 -0
  101. package/dist/client/workflow/stepTypes.js +3 -0
  102. package/dist/client/workflow/store.d.ts +17 -17
  103. package/dist/client/workflow/store.js +65 -100
  104. package/dist/client/workflow/storeExecutionCodec.d.ts +11 -0
  105. package/dist/client/workflow/storeExecutionCodec.js +34 -0
  106. package/dist/client/workflow/storeListing.d.ts +11 -0
  107. package/dist/client/workflow/storeListing.js +45 -0
  108. package/dist/client/workflow/storeMaintenance.d.ts +4 -0
  109. package/dist/client/workflow/storeMaintenance.js +40 -0
  110. package/dist/client/workflow/storeSignals.d.ts +9 -0
  111. package/dist/client/workflow/storeSignals.js +38 -2
  112. package/dist/client/workflow/subWorkflowRunner.d.ts +13 -0
  113. package/dist/client/workflow/subWorkflowRunner.js +40 -0
  114. package/dist/client/workflow/types.d.ts +4 -356
  115. package/dist/client/workflow/types.js +1 -3
  116. package/dist/client/workflow/waitFor.js +39 -26
  117. package/dist/client/workflow/workflow.d.ts +16 -59
  118. package/dist/client/workflow/workflow.js +53 -179
  119. package/dist/client/workflow/workflowDecisions.d.ts +11 -0
  120. package/dist/client/workflow/workflowDecisions.js +27 -0
  121. package/dist/client/workflow/workflowDefinition.d.ts +16 -0
  122. package/dist/client/workflow/workflowDefinition.js +123 -0
  123. package/dist/client/workflow/workflowIntrospection.d.ts +5 -0
  124. package/dist/client/workflow/workflowIntrospection.js +46 -0
  125. package/dist/client/workflow/workflowValidation.d.ts +44 -0
  126. package/dist/client/workflow/workflowValidation.js +143 -0
  127. package/dist/domain/types/command.d.ts +7 -1
  128. package/dist/domain/types/flow.d.ts +25 -0
  129. package/dist/domain/types/flow.js +1 -0
  130. package/dist/infrastructure/persistence/dependencyCompletionSchema.d.ts +6 -0
  131. package/dist/infrastructure/persistence/dependencyCompletionSchema.js +16 -0
  132. package/dist/infrastructure/persistence/dependencyCompletionStore.d.ts +38 -0
  133. package/dist/infrastructure/persistence/dependencyCompletionStore.js +105 -0
  134. package/dist/infrastructure/persistence/schema.d.ts +2 -5
  135. package/dist/infrastructure/persistence/schema.js +57 -3
  136. package/dist/infrastructure/persistence/sqlite.d.ts +35 -0
  137. package/dist/infrastructure/persistence/sqlite.js +238 -12
  138. package/dist/infrastructure/persistence/sqliteBatch.js +10 -4
  139. package/dist/infrastructure/persistence/sqliteSerializer.d.ts +5 -0
  140. package/dist/infrastructure/persistence/sqliteSerializer.js +28 -5
  141. package/dist/infrastructure/persistence/statements.d.ts +4 -0
  142. package/dist/infrastructure/persistence/statements.js +8 -2
  143. package/dist/infrastructure/server/handlerRoutes.js +3 -0
  144. package/dist/infrastructure/server/handlers/advanced.js +2 -2
  145. package/dist/infrastructure/server/handlers/flow.d.ts +7 -0
  146. package/dist/infrastructure/server/handlers/flow.js +11 -0
  147. package/dist/infrastructure/server/handlers/index.d.ts +1 -0
  148. package/dist/infrastructure/server/handlers/index.js +1 -0
  149. package/package.json +4 -2
@@ -13,8 +13,9 @@ import { WorkerManager } from './workerManager';
13
13
  import { EventsManager } from './eventsManager';
14
14
  import { createMonitoringState } from './monitoringChecks';
15
15
  import { RWLock, withWriteLock } from '../shared/lock';
16
- import { shardIndex, SHARD_COUNT } from '../shared/hash';
16
+ import { processingShardIndex, shardIndex, SHARD_COUNT } from '../shared/hash';
17
17
  import { pushJob, pushJobBatch } from './operations/push';
18
+ import { pushFlowBatch } from './operations/flowPush';
18
19
  import { pullJob, pullJobBatch } from './operations/pull';
19
20
  import { ackJob, ackJobBatch, ackJobBatchWithResults, failJob } from './operations/ack';
20
21
  import * as queueControl from './operations/queueControl';
@@ -35,6 +36,9 @@ import { ContextFactory } from './contextFactory';
35
36
  import { processPendingDependencies } from './dependencyProcessor';
36
37
  import { handleTaskError, handleTaskSuccess } from './taskErrorTracking';
37
38
  import { DependencyResultTracker } from './dependencyResultTracker';
39
+ import { recoverFlowFailures } from './flowFailureRecovery';
40
+ import { assertFlowParentOwnership, backpatchDeclaredFlowChild, canAcceptRemovedFlowChild, flowChildFailureError, isDeclaredFlowChild, } from './flowParentBackpatch';
41
+ import { commitRemovedCompletion, DependencyCompletionTracker, reconcileDependencyCompletionPins, releaseDependencyCompletionPins, } from './dependencyCompletions';
38
42
  /**
39
43
  * QueueManager - Central coordinator
40
44
  */
@@ -44,15 +48,15 @@ export class QueueManager {
44
48
  // Sharded data structures
45
49
  shards = [];
46
50
  shardLocks = [];
51
+ customIdLock = new RWLock();
47
52
  processingShards = [];
48
53
  processingLocks = [];
49
54
  // Global indexes (bounded with LRU eviction)
50
55
  jobIndex = new Map();
51
56
  completedJobs;
52
57
  completedJobsData;
53
- // Bare completion ids of removeOnComplete jobs kept ONLY so dependent jobs
54
- // can unblock (no payload, not surfaced in state/stats). Bounded like
55
- // completedJobs; entries are pruned by the dependency processor once consumed.
58
+ // Bare removeOnComplete evidence: bounded recent IDs plus IDs pinned by live
59
+ // dependency edges. It is never surfaced as completed job data or statistics.
56
60
  depCompletions;
57
61
  // Ids of jobs failed by the timeout sweep. A late ACK whose lock token no
58
62
  // longer matches (the job was requeued for retry) is discarded for these,
@@ -125,7 +129,9 @@ export class QueueManager {
125
129
  this.jobIndex.delete(jobId);
126
130
  this.completedJobsData.delete(jobId);
127
131
  });
128
- this.depCompletions = new BoundedSet(this.config.maxCompletedJobs);
132
+ this.depCompletions = new DependencyCompletionTracker(this.config.maxCompletedJobs, (jobId) => {
133
+ this.storage?.deleteDependencyCompletion(jobId);
134
+ });
129
135
  this.timedOutJobs = new BoundedSet(this.config.maxCompletedJobs);
130
136
  this.perQueueMetrics = new LRUMap(this.config.maxCustomIds);
131
137
  this.jobResults = new LRUMap(this.config.maxJobResults);
@@ -162,6 +168,19 @@ export class QueueManager {
162
168
  this.contextFactory = new ContextFactory(this.getContextDependencies(), this.getContextCallbacks());
163
169
  // Load and start
164
170
  bgTasks.recover(this.contextFactory.getBackgroundContext());
171
+ if (this.storage) {
172
+ recoverFlowFailures({
173
+ storage: this.storage,
174
+ shards: this.shards,
175
+ jobIndex: this.jobIndex,
176
+ completedJobs: this.completedJobs,
177
+ depCompletions: this.depCompletions,
178
+ maxDependencyCompletions: this.config.maxCompletedJobs,
179
+ dependencyResults: this.dependencyResults,
180
+ failedChildrenValues: this.failedChildrenValues,
181
+ ignoredChildrenFailures: this.ignoredChildrenFailures,
182
+ });
183
+ }
165
184
  this.recoveryStats = { queues: this.queueNamesCache.size, jobs: this.jobIndex.size };
166
185
  if (this.storage) {
167
186
  this.cronScheduler.load(this.storage.loadCronJobs());
@@ -175,6 +194,7 @@ export class QueueManager {
175
194
  storage: this.storage,
176
195
  shards: this.shards,
177
196
  shardLocks: this.shardLocks,
197
+ customIdLock: this.customIdLock,
178
198
  processingShards: this.processingShards,
179
199
  processingLocks: this.processingLocks,
180
200
  jobIndex: this.jobIndex,
@@ -208,6 +228,7 @@ export class QueueManager {
208
228
  registerQueueName: this.registerQueueName.bind(this),
209
229
  unregisterQueueName: this.unregisterQueueName.bind(this),
210
230
  onJobCompleted: this.onJobCompleted.bind(this),
231
+ onJobFailed: this.onJobFailed.bind(this),
211
232
  onJobsCompleted: this.onJobsCompleted.bind(this),
212
233
  hasPendingDeps: this.hasPendingDeps.bind(this),
213
234
  onRepeat: this.handleRepeat.bind(this),
@@ -260,6 +281,9 @@ export class QueueManager {
260
281
  this.registerQueueName(queue);
261
282
  return pushJobBatch(queue, inputs, this.contextFactory.getPushContext());
262
283
  }
284
+ async pushFlow(batch) {
285
+ return pushFlowBatch(batch, this.contextFactory.getPushContext());
286
+ }
263
287
  async pull(queue, timeoutMs = 0, signal) {
264
288
  return pullJob(queue, timeoutMs, this.contextFactory.getPullContext(), signal);
265
289
  }
@@ -545,8 +569,8 @@ export class QueueManager {
545
569
  ctx.storage?.markCompleted(jId, Date.now(), job.timeline);
546
570
  }
547
571
  else {
572
+ commitRemovedCompletion(job, ctx);
548
573
  ctx.jobIndex.delete(jId);
549
- ctx.storage?.deleteJob(jId);
550
574
  }
551
575
  if (result !== undefined)
552
576
  ctx.dependencyResults.retain(jId, result);
@@ -665,49 +689,112 @@ export class QueueManager {
665
689
  * Used by FlowProducer when creating flows where children need to reference parent.
666
690
  */
667
691
  async updateJobParent(childJobId, parentJobId) {
668
- const childJob = await this.getJob(childJobId);
669
- if (!childJob)
670
- return;
692
+ if (childJobId === parentJobId)
693
+ throw new Error('A flow job cannot be its own parent');
694
+ let childJob = await this.getJob(childJobId);
671
695
  const parentJob = await this.getJob(parentJobId);
672
- // Update domain parentId field (cast to bypass readonly for flow linkage)
673
- childJob.parentId = parentJobId;
674
- // Update the job's data to include parent reference. __parentQueue must be
675
- // the PARENT's queue it feeds buildParentKey/extractParent/buildParentOpts
676
- // for child→parent navigation (Job.parent, .parentKey, .opts.parent, toJSON).
677
- // Using the child's own queue corrupted cross-queue flows (audit #102 follow-up).
678
- const jobData = childJob.data;
679
- jobData.__parentId = parentJobId;
680
- jobData.__parentQueue = parentJob?.queue ?? jobData.__parentQueue ?? childJob.queue;
681
- // Also add this child to parent's childrenIds
682
- if (parentJob) {
683
- if (!parentJob.childrenIds) {
684
- parentJob.childrenIds = [];
696
+ if (!childJob) {
697
+ if (canAcceptRemovedFlowChild(parentJob, childJobId, this.depCompletions))
698
+ return;
699
+ throw new Error(`Child job not found: ${String(childJobId)}`);
700
+ }
701
+ if (!parentJob)
702
+ throw new Error(`Parent job not found: ${String(parentJobId)}`);
703
+ assertFlowParentOwnership(childJob, parentJobId);
704
+ if (isDeclaredFlowChild(parentJob, childJobId)) {
705
+ childJob = await backpatchDeclaredFlowChild(childJob, parentJob, {
706
+ storage: this.storage,
707
+ customIdLock: this.customIdLock,
708
+ shards: this.shards,
709
+ shardLocks: this.shardLocks,
710
+ processingShards: this.processingShards,
711
+ processingLocks: this.processingLocks,
712
+ jobIndex: this.jobIndex,
713
+ completedJobsData: this.completedJobsData,
714
+ depCompletions: this.depCompletions,
715
+ });
716
+ }
717
+ else {
718
+ const parentLocation = this.jobIndex.get(parentJobId);
719
+ if (parentLocation?.type !== 'queue') {
720
+ throw new Error(`Parent job ${String(parentJobId)} is not linkable`);
685
721
  }
686
- if (!parentJob.childrenIds.includes(childJobId)) {
687
- parentJob.childrenIds.push(childJobId);
722
+ const childLocation = this.jobIndex.get(childJobId);
723
+ const shardIndexes = [
724
+ ...new Set([shardIndex(childJob.queue), shardIndex(parentJob.queue)]),
725
+ ].sort((a, b) => a - b);
726
+ const processingIndexes = childLocation?.type === 'processing' ? [processingShardIndex(childJobId)] : [];
727
+ const guards = [await this.customIdLock.acquireWrite()];
728
+ try {
729
+ for (const index of shardIndexes)
730
+ guards.push(await this.shardLocks[index].acquireWrite());
731
+ for (const index of processingIndexes) {
732
+ guards.push(await this.processingLocks[index].acquireWrite());
733
+ }
734
+ if (this.jobIndex.get(parentJobId)?.type !== 'queue') {
735
+ throw new Error(`Parent job ${String(parentJobId)} changed state while linking`);
736
+ }
737
+ const childData = {
738
+ ...childJob.data,
739
+ __parentId: String(parentJobId),
740
+ __parentQueue: parentJob.queue,
741
+ };
742
+ const childrenIds = [...parentJob.childrenIds, childJobId];
743
+ const dependsOn = parentJob.dependsOn.includes(childJobId)
744
+ ? [...parentJob.dependsOn]
745
+ : [...parentJob.dependsOn, childJobId];
746
+ const parentData = {
747
+ ...parentJob.data,
748
+ __childrenIds: childrenIds.map(String),
749
+ };
750
+ const linkedChild = { ...childJob, data: childData, parentId: parentJobId };
751
+ const linkedParent = { ...parentJob, data: parentData, childrenIds, dependsOn };
752
+ const childFinished = this.completedJobs.has(childJobId) || this.depCompletions.has(childJobId);
753
+ const parentState = childFinished
754
+ ? parentJob.runAt > Date.now()
755
+ ? 'delayed'
756
+ : parentJob.priority > 0
757
+ ? 'prioritized'
758
+ : 'waiting'
759
+ : 'waiting-children';
760
+ this.storage?.updateFlowLink(linkedChild, linkedParent, parentState);
761
+ childJob.parentId = parentJobId;
762
+ childJob.data = childData;
763
+ parentJob.childrenIds = childrenIds;
764
+ parentJob.dependsOn = dependsOn;
765
+ parentJob.data = parentData;
766
+ if (!childFinished) {
767
+ const shard = this.shards[shardIndex(parentJob.queue)];
768
+ if (!shard.waitingDeps.has(parentJobId)) {
769
+ const removed = shard.getQueue(parentJob.queue).remove(parentJobId);
770
+ if (removed)
771
+ shard.decrementQueued(parentJobId);
772
+ shard.waitingDeps.set(parentJobId, parentJob);
773
+ }
774
+ shard.registerDependencies(parentJobId, [childJobId]);
775
+ this.dependencyResults.registerConsumer(parentJobId, dependsOn);
776
+ }
688
777
  }
689
- }
690
- // Persist changes to SQLite if storage is available
691
- if (this.storage) {
692
- this.storage.updateJobData(childJobId, childJob.data);
693
- if (parentJob) {
694
- this.storage.updateJobChildrenIds(parentJobId, parentJob.childrenIds);
778
+ finally {
779
+ for (let index = guards.length - 1; index >= 0; index--)
780
+ guards[index].release();
695
781
  }
696
782
  }
697
783
  // Handle race condition: child may have already terminally failed
698
784
  // before parent linkage was established (parentId was 'pending').
699
785
  // If so, propagate failParentOnFailure now with the real parent ID.
700
786
  const childLoc = this.jobIndex.get(childJobId);
787
+ const childFailureError = childLoc?.type === 'dlq'
788
+ ? (flowChildFailureError(childJob, this.shards) ?? 'Child job failed')
789
+ : 'Child job failed';
701
790
  if (childLoc?.type === 'dlq' && childJob.failParentOnFailure) {
702
- this.moveParentToFailed(parentJobId, childJob, 'Child job failed').catch(() => {
703
- // Best-effort: parent may already be in DLQ or removed
704
- });
791
+ await this.moveParentToFailed(parentJobId, childJob, childFailureError);
705
792
  }
706
793
  if (childLoc?.type === 'dlq' &&
707
794
  (childJob.removeDependencyOnFailure ||
708
795
  childJob.ignoreDependencyOnFailure ||
709
796
  childJob.continueParentOnFailure)) {
710
- this.onChildDependencyOption(childJob, 'Child job failed');
797
+ await this.onChildDependencyOption(childJob, childFailureError);
711
798
  }
712
799
  }
713
800
  getJobByCustomId(customId) {
@@ -803,6 +890,12 @@ export class QueueManager {
803
890
  }
804
891
  for (const cid of customIdsToDelete)
805
892
  this.customIdMap.delete(cid);
893
+ // removeOnComplete jobs have no jobIndex entry, but their bounded durable
894
+ // dependency proofs still belong to the obliterated queue.
895
+ const removedCompletions = this.storage?.deleteDependencyCompletionsForQueue(queue) ?? [];
896
+ for (const jobId of removedCompletions)
897
+ this.depCompletions.delete(jobId);
898
+ this.reconcileCompletionPins();
806
899
  // Per-queue cumulative counters are keyed by queue name and never expire on
807
900
  // their own; obliterate is the documented way to reclaim ALL state for a
808
901
  // queue, so drop its metrics entry too (prevents unbounded growth for
@@ -834,6 +927,22 @@ export class QueueManager {
834
927
  unregisterQueueName(queue) {
835
928
  this.queueNamesCache.delete(queue);
836
929
  }
930
+ releaseCompletionPins(dependencyIds) {
931
+ releaseDependencyCompletionPins(dependencyIds, {
932
+ storage: this.storage,
933
+ shards: this.shards,
934
+ depCompletions: this.depCompletions,
935
+ maxDependencyCompletions: this.config.maxCompletedJobs,
936
+ });
937
+ }
938
+ reconcileCompletionPins() {
939
+ reconcileDependencyCompletionPins({
940
+ storage: this.storage,
941
+ shards: this.shards,
942
+ depCompletions: this.depCompletions,
943
+ maxDependencyCompletions: this.config.maxCompletedJobs,
944
+ });
945
+ }
837
946
  clean(queue, graceMs, state, limit) {
838
947
  return queueControl.cleanQueue(queue, graceMs, this.contextFactory.getQueueControlContext(), state, limit);
839
948
  }
@@ -1221,10 +1330,17 @@ export class QueueManager {
1221
1330
  // leaked an entry permanently.
1222
1331
  this.failedChildrenValues.delete(completedId);
1223
1332
  this.ignoredChildrenFailures.delete(completedId);
1333
+ this.storage?.deleteFlowFailure(completedId);
1224
1334
  this.pendingDepChecks.add(completedId);
1225
1335
  this.scheduleDependencyFlush();
1226
1336
  void this.checkFlowCompleted(completedId);
1227
1337
  }
1338
+ /** Release failure metadata owned by a parent that reached terminal failure. */
1339
+ onJobFailed(failedId) {
1340
+ this.failedChildrenValues.delete(failedId);
1341
+ this.ignoredChildrenFailures.delete(failedId);
1342
+ this.storage?.deleteFlowFailure(failedId);
1343
+ }
1228
1344
  /** Check if completing this job completes an entire flow */
1229
1345
  async checkFlowCompleted(completedId) {
1230
1346
  const job = await this.getJob(completedId);
@@ -1246,13 +1362,11 @@ export class QueueManager {
1246
1362
  * BullMQ v5: failParentOnFailure — when a child terminally fails
1247
1363
  * and has failParentOnFailure: true, also fail the parent job.
1248
1364
  */
1249
- failParentOnChildFailure(childJob, error) {
1365
+ async failParentOnChildFailure(childJob, error) {
1250
1366
  const parentId = childJob.parentId;
1251
1367
  if (!parentId)
1252
1368
  return;
1253
- this.moveParentToFailed(parentId, childJob, error).catch(() => {
1254
- // Best-effort: parent may already be in DLQ or removed
1255
- });
1369
+ await this.moveParentToFailed(parentId, childJob, error);
1256
1370
  }
1257
1371
  /** Move a parent job to DLQ/failed state because a child failed */
1258
1372
  async moveParentToFailed(parentId, childJob, error) {
@@ -1266,6 +1380,7 @@ export class QueueManager {
1266
1380
  if (parentLoc.type !== 'queue')
1267
1381
  return;
1268
1382
  const idx = shardIndex(parentJob.queue);
1383
+ let releasedDependencies = [];
1269
1384
  await withWriteLock(this.shardLocks[idx], () => {
1270
1385
  // Re-check inside lock to prevent duplicate DLQ entries (TOCTOU guard)
1271
1386
  if (this.jobIndex.get(parentId)?.type !== 'queue')
@@ -1273,6 +1388,7 @@ export class QueueManager {
1273
1388
  const shard = this.shards[idx];
1274
1389
  // Remove from waitingDeps if present
1275
1390
  if (shard.waitingDeps.has(parentId)) {
1391
+ releasedDependencies = [...parentJob.dependsOn];
1276
1392
  shard.waitingDeps.delete(parentId);
1277
1393
  shard.unregisterDependencies(parentId, parentJob.dependsOn);
1278
1394
  }
@@ -1292,7 +1408,9 @@ export class QueueManager {
1292
1408
  this.jobIndex.set(parentId, { type: 'dlq', queueName: parentJob.queue });
1293
1409
  this.storage?.saveDlqEntry(entry);
1294
1410
  this.storage?.deleteJob(parentId);
1411
+ this.storage?.deleteFlowFailure(parentId, childJob.id);
1295
1412
  });
1413
+ this.releaseCompletionPins(releasedDependencies);
1296
1414
  // Parent reached a terminal (DLQ) state — release its flow-failure tracking.
1297
1415
  this.failedChildrenValues.delete(parentId);
1298
1416
  this.ignoredChildrenFailures.delete(parentId);
@@ -1316,19 +1434,15 @@ export class QueueManager {
1316
1434
  /**
1317
1435
  * Handle child dependency options: removeDependencyOnFailure, ignoreDependencyOnFailure, continueParentOnFailure
1318
1436
  */
1319
- onChildDependencyOption(childJob, error) {
1437
+ async onChildDependencyOption(childJob, error) {
1320
1438
  if (!childJob.parentId)
1321
1439
  return;
1322
1440
  if (childJob.continueParentOnFailure) {
1323
- this.continueParentOnChildFailure(childJob, error).catch(() => {
1324
- // Best-effort parent propagation; child failure is already durable.
1325
- });
1441
+ await this.continueParentOnChildFailure(childJob, error);
1326
1442
  }
1327
1443
  else {
1328
1444
  // removeDependencyOnFailure or ignoreDependencyOnFailure
1329
- this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure).catch(() => {
1330
- // Best-effort parent propagation; child failure is already durable.
1331
- });
1445
+ await this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure);
1332
1446
  }
1333
1447
  }
1334
1448
  /**
@@ -1351,6 +1465,21 @@ export class QueueManager {
1351
1465
  existing[childKey] = error ?? 'unknown error';
1352
1466
  this.failedChildrenValues.set(parentId, existing);
1353
1467
  const idx = shardIndex(parentJob.queue);
1468
+ let releasedDependencies = [];
1469
+ await withWriteLock(this.shardLocks[idx], () => {
1470
+ if (this.jobIndex.get(parentId)?.type !== 'queue')
1471
+ return;
1472
+ const shard = this.shards[idx];
1473
+ const dependencies = [...parentJob.dependsOn];
1474
+ releasedDependencies = dependencies;
1475
+ shard.unregisterDependencies(parentId, dependencies);
1476
+ for (const dependency of dependencies) {
1477
+ this.dependencyResults.releaseDependency(parentId, dependency);
1478
+ }
1479
+ parentJob.dependsOn = [];
1480
+ this.storage?.updateFlowParentResolution(parentJob);
1481
+ });
1482
+ this.releaseCompletionPins(releasedDependencies);
1354
1483
  await this.promoteParentAfterChildFailure(parentId, parentJob, idx);
1355
1484
  }
1356
1485
  /**
@@ -1365,6 +1494,7 @@ export class QueueManager {
1365
1494
  timer.unref?.();
1366
1495
  });
1367
1496
  let promoted = false;
1497
+ let releasedDependencies = [];
1368
1498
  await withWriteLock(this.shardLocks[idx], () => {
1369
1499
  // TOCTOU guard
1370
1500
  if (this.jobIndex.get(parentId)?.type !== 'queue')
@@ -1372,6 +1502,7 @@ export class QueueManager {
1372
1502
  const shard = this.shards[idx];
1373
1503
  // Remove from waitingDeps
1374
1504
  if (shard.waitingDeps.has(parentId)) {
1505
+ releasedDependencies = [...parentJob.dependsOn];
1375
1506
  shard.waitingDeps.delete(parentId);
1376
1507
  shard.unregisterDependencies(parentId, parentJob.dependsOn);
1377
1508
  }
@@ -1387,10 +1518,12 @@ export class QueueManager {
1387
1518
  queue.push(parentJob);
1388
1519
  shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
1389
1520
  this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
1521
+ this.storage?.updateFlowParentResolution(parentJob);
1390
1522
  shard.notify(parentJob.queue);
1391
1523
  promoted = true;
1392
1524
  }
1393
1525
  });
1526
+ this.releaseCompletionPins(releasedDependencies);
1394
1527
  if (promoted) {
1395
1528
  this.eventsManager.broadcast({
1396
1529
  eventType: 'waiting',
@@ -1426,6 +1559,7 @@ export class QueueManager {
1426
1559
  // Remove the failed child from the parent's pending deps synchronously so
1427
1560
  // dependency tracking stays consistent; defer only the promotion decision.
1428
1561
  let readyToPromote = false;
1562
+ let releasedDependency = null;
1429
1563
  await withWriteLock(this.shardLocks[idx], () => {
1430
1564
  if (this.jobIndex.get(parentId)?.type !== 'queue')
1431
1565
  return;
@@ -1438,16 +1572,22 @@ export class QueueManager {
1438
1572
  if (depIndex !== -1) {
1439
1573
  parentJob.dependsOn.splice(depIndex, 1);
1440
1574
  shard.unregisterDependencies(parentId, [childJob.id]);
1575
+ releasedDependency = childJob.id;
1441
1576
  this.dependencyResults.releaseDependency(parentId, childJob.id);
1577
+ this.storage?.updateFlowParentResolution(parentJob);
1442
1578
  }
1443
1579
  // If no more pending deps, the parent is ready to be promoted
1444
1580
  readyToPromote =
1445
1581
  parentJob.dependsOn.length === 0 ||
1446
1582
  parentJob.dependsOn.every((dep) => this.completedJobs.has(dep));
1447
1583
  });
1584
+ if (releasedDependency)
1585
+ this.releaseCompletionPins([releasedDependency]);
1448
1586
  if (readyToPromote) {
1449
1587
  await this.promoteParentAfterChildFailure(parentId, parentJob, idx);
1450
1588
  }
1589
+ if (!storeIgnored)
1590
+ this.storage?.deleteFlowFailure(parentId, childJob.id);
1451
1591
  }
1452
1592
  /**
1453
1593
  * Get failed children values for a parent job (populated by continueParentOnFailure).
@@ -1479,34 +1619,88 @@ export class QueueManager {
1479
1619
  const parentLoc = this.jobIndex.get(parentId);
1480
1620
  if (parentLoc?.type !== 'queue')
1481
1621
  return false;
1482
- const idx = shardIndex(parentJob.queue);
1622
+ const childLoc = this.jobIndex.get(childJobId);
1623
+ if (childLoc?.type !== 'queue' && childLoc?.type !== 'processing')
1624
+ return false;
1625
+ const parentIdx = shardIndex(parentJob.queue);
1626
+ const shardIndexes = [...new Set([parentIdx, shardIndex(childJob.queue)])].sort((a, b) => a - b);
1627
+ const processingIndexes = childLoc.type === 'processing' ? [processingShardIndex(childJobId)] : [];
1628
+ const guards = [];
1629
+ let removed = false;
1483
1630
  let promoted = false;
1484
- await withWriteLock(this.shardLocks[idx], () => {
1485
- if (this.jobIndex.get(parentId)?.type !== 'queue')
1486
- return;
1487
- const shard = this.shards[idx];
1488
- const parentInDeps = shard.waitingDeps.get(parentId);
1489
- if (!parentInDeps)
1490
- return;
1491
- const depIndex = parentJob.dependsOn.indexOf(childJobId);
1492
- if (depIndex !== -1) {
1493
- parentJob.dependsOn.splice(depIndex, 1);
1494
- shard.unregisterDependencies(parentId, [childJobId]);
1495
- this.dependencyResults.releaseDependency(parentId, childJobId);
1631
+ let releasedDependencies = [];
1632
+ try {
1633
+ for (const index of shardIndexes)
1634
+ guards.push(await this.shardLocks[index].acquireWrite());
1635
+ for (const index of processingIndexes) {
1636
+ guards.push(await this.processingLocks[index].acquireWrite());
1637
+ }
1638
+ if (this.jobIndex.get(parentId)?.type !== 'queue' ||
1639
+ this.jobIndex.get(childJobId)?.type !== childLoc.type) {
1640
+ return false;
1641
+ }
1642
+ const shard = this.shards[parentIdx];
1643
+ if (!shard.waitingDeps.has(parentId) || !parentJob.dependsOn.includes(childJobId)) {
1644
+ return false;
1496
1645
  }
1497
- const allDone = parentJob.dependsOn.length === 0 ||
1498
- parentJob.dependsOn.every((dep) => this.completedJobs.has(dep));
1499
- if (allDone) {
1646
+ const unresolved = parentJob.dependsOn.filter((dependency) => dependency !== childJobId &&
1647
+ !this.completedJobs.has(dependency) &&
1648
+ !this.depCompletions.has(dependency));
1649
+ const released = parentJob.dependsOn.filter((dependency) => !unresolved.includes(dependency));
1650
+ releasedDependencies = released;
1651
+ const childrenIds = parentJob.childrenIds.filter((childId) => childId !== childJobId);
1652
+ const childData = { ...childJob.data };
1653
+ delete childData.__parentId;
1654
+ delete childData.__parentQueue;
1655
+ const parentData = { ...parentJob.data };
1656
+ if (childrenIds.length > 0)
1657
+ parentData.__childrenIds = childrenIds.map(String);
1658
+ else
1659
+ delete parentData.__childrenIds;
1660
+ const runAt = unresolved.length === 0 ? Date.now() : parentJob.runAt;
1661
+ const parentState = unresolved.length > 0
1662
+ ? 'waiting-children'
1663
+ : parentJob.priority > 0
1664
+ ? 'prioritized'
1665
+ : 'waiting';
1666
+ const detachedChild = { ...childJob, parentId: null, data: childData };
1667
+ const detachedParent = {
1668
+ ...parentJob,
1669
+ childrenIds,
1670
+ dependsOn: unresolved,
1671
+ data: parentData,
1672
+ runAt,
1673
+ };
1674
+ this.storage?.removeFlowLink(detachedChild, detachedParent, parentState);
1675
+ childJob.parentId = null;
1676
+ childJob.data = childData;
1677
+ parentJob.childrenIds = childrenIds;
1678
+ parentJob.dependsOn = unresolved;
1679
+ parentJob.data = parentData;
1680
+ parentJob.runAt = runAt;
1681
+ shard.unregisterDependencies(parentId, released);
1682
+ for (const dependency of released) {
1683
+ this.dependencyResults.releaseDependency(parentId, dependency);
1684
+ }
1685
+ if (unresolved.length === 0) {
1500
1686
  shard.waitingDeps.delete(parentId);
1501
- const now = Date.now();
1502
- parentJob.runAt = now;
1503
1687
  shard.getQueue(parentJob.queue).push(parentJob);
1504
- shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
1505
- this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
1688
+ shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, runAt);
1689
+ this.jobIndex.set(parentId, {
1690
+ type: 'queue',
1691
+ shardIdx: parentIdx,
1692
+ queueName: parentJob.queue,
1693
+ });
1506
1694
  shard.notify(parentJob.queue);
1507
1695
  promoted = true;
1508
1696
  }
1509
- });
1697
+ removed = true;
1698
+ }
1699
+ finally {
1700
+ for (let index = guards.length - 1; index >= 0; index--)
1701
+ guards[index].release();
1702
+ }
1703
+ this.releaseCompletionPins(releasedDependencies);
1510
1704
  if (promoted) {
1511
1705
  this.eventsManager.broadcast({
1512
1706
  eventType: 'waiting',
@@ -1516,7 +1710,7 @@ export class QueueManager {
1516
1710
  prev: 'waiting-children',
1517
1711
  });
1518
1712
  }
1519
- return true;
1713
+ return removed;
1520
1714
  }
1521
1715
  /**
1522
1716
  * Remove all unprocessed (waiting/delayed) children of a parent job.
@@ -13,6 +13,7 @@ import type { WebhookManager } from './webhookManager';
13
13
  import type { WorkerManager } from './workerManager';
14
14
  import type { MonitoringState } from './monitoringChecks';
15
15
  import type { DependencyResultTracker } from './dependencyResultTracker';
16
+ import type { DependencyCompletionTracker } from './dependencyCompletions';
16
17
  /** Queue Manager configuration */
17
18
  export interface QueueManagerConfig {
18
19
  dataPath?: string;
@@ -108,7 +109,8 @@ export interface BackgroundContext extends QueueManagerState {
108
109
  workerManager: WorkerManager;
109
110
  monitoringState: MonitoringState;
110
111
  completedJobsData: BoundedMap<JobId, Job>;
111
- depCompletions?: BoundedSet<JobId>;
112
+ depCompletions?: DependencyCompletionTracker;
113
+ maxDependencyCompletions: number;
112
114
  timedOutJobs?: BoundedSet<JobId>;
113
115
  }
114
116
  /** Context for stats operations */
@@ -1,3 +1,3 @@
1
- export declare const CLI_NETWORK_COMMANDS: readonly ("push" | "queue" | "dlq" | "job" | "cron" | "worker" | "concurrency" | "ping" | "stats" | "metrics" | "pull" | "ack" | "fail" | "rate-limit" | "health" | "webhook")[];
1
+ export declare const CLI_NETWORK_COMMANDS: readonly ("push" | "fail" | "queue" | "dlq" | "job" | "cron" | "worker" | "concurrency" | "ping" | "stats" | "metrics" | "pull" | "ack" | "rate-limit" | "health" | "webhook")[];
2
2
  /** Build the protocol command for a routed network CLI command. */
3
3
  export declare function buildCommand(command: string, args: string[]): Promise<Record<string, unknown> | null>;
@@ -34,6 +34,7 @@ export declare class FlowProducer extends EventEmitter {
34
34
  private readonly embedded;
35
35
  private readonly tcp;
36
36
  private readonly useSharedPool;
37
+ private closed;
37
38
  constructor(opts?: FlowProducerOptions);
38
39
  /** Get push context for helper functions */
39
40
  private get pushCtx();
@@ -70,10 +71,6 @@ export declare class FlowProducer extends EventEmitter {
70
71
  getParentResults<R = unknown>(parentIds: string[]): Map<string, R>;
71
72
  /** Build callbacks that wire flow job methods to actual management operations */
72
73
  private buildCallbacks;
73
- private getFlowEmbedded;
74
- private getFlowTcp;
75
- private buildJobNode;
76
- private buildJobNodeFromTcp;
77
- private addFlowNode;
78
- private addTreeNode;
74
+ private buildPlannedNode;
75
+ private indexSnapshots;
79
76
  }