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
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * SQLite schema and migrations
3
3
  */
4
+ import { DEPENDENCY_COMPLETION_SCHEMA } from './dependencyCompletionSchema';
4
5
  /** SQLite PRAGMA settings for optimal performance */
5
6
  export const PRAGMA_SETTINGS = `
6
7
  PRAGMA journal_mode = WAL;
@@ -42,6 +43,10 @@ CREATE TABLE IF NOT EXISTS jobs (
42
43
  progress_msg TEXT,
43
44
  remove_on_complete INTEGER DEFAULT 0,
44
45
  remove_on_fail INTEGER DEFAULT 0,
46
+ fail_parent_on_failure INTEGER NOT NULL DEFAULT 0,
47
+ remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
48
+ continue_parent_on_failure INTEGER NOT NULL DEFAULT 0,
49
+ ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
45
50
  stall_timeout INTEGER,
46
51
  last_heartbeat INTEGER,
47
52
  stall_count INTEGER NOT NULL DEFAULT 0,
@@ -58,7 +63,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_queue_created
58
63
  CREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created
59
64
  ON jobs(queue, state, created_at, id);
60
65
  CREATE INDEX IF NOT EXISTS idx_jobs_run_at
61
- ON jobs(run_at) WHERE state IN ('waiting', 'delayed');
66
+ ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
62
67
  CREATE INDEX IF NOT EXISTS idx_jobs_unique
63
68
  ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;
64
69
  CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
@@ -66,6 +71,20 @@ CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
66
71
  CREATE INDEX IF NOT EXISTS idx_jobs_parent
67
72
  ON jobs(parent_id) WHERE parent_id IS NOT NULL;
68
73
 
74
+ -- Durable outbox/state for terminal child failure propagation.
75
+ CREATE TABLE IF NOT EXISTS flow_failures (
76
+ parent_id TEXT NOT NULL,
77
+ child_id TEXT NOT NULL,
78
+ child_queue TEXT NOT NULL,
79
+ mode TEXT NOT NULL,
80
+ error TEXT NOT NULL,
81
+ created_at INTEGER NOT NULL,
82
+ PRIMARY KEY (parent_id, child_id)
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
85
+
86
+ ${DEPENDENCY_COMPLETION_SCHEMA}
87
+
69
88
  -- Job results storage (BLOB for MessagePack)
70
89
  CREATE TABLE IF NOT EXISTS job_results (
71
90
  job_id TEXT PRIMARY KEY,
@@ -97,7 +116,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_group_id
97
116
 
98
117
  -- Pending jobs: compound index for priority-ordered retrieval
99
118
  CREATE INDEX IF NOT EXISTS idx_jobs_pending_priority
100
- ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');
119
+ ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
101
120
 
102
121
  -- Completed jobs: index for recovery ordering (issue #84)
103
122
  CREATE INDEX IF NOT EXISTS idx_jobs_completed_order
@@ -146,7 +165,7 @@ CREATE TABLE IF NOT EXISTS migrations (
146
165
  );
147
166
  `;
148
167
  /** Current schema version */
149
- export const SCHEMA_VERSION = 22;
168
+ export const SCHEMA_VERSION = 29;
150
169
  /** All migrations in order */
151
170
  export const MIGRATIONS = {
152
171
  1: SCHEMA,
@@ -241,4 +260,39 @@ ALTER TABLE queue_state ADD COLUMN stall_grace_period INTEGER;
241
260
  22: `
242
261
  ALTER TABLE queue_state ADD COLUMN dlq_config BLOB;
243
262
  `,
263
+ // Migrations 23-26: flow failure policy must survive active-job recovery.
264
+ 23: `
265
+ ALTER TABLE jobs ADD COLUMN fail_parent_on_failure INTEGER NOT NULL DEFAULT 0;
266
+ `,
267
+ 24: `
268
+ ALTER TABLE jobs ADD COLUMN remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
269
+ `,
270
+ 25: `
271
+ ALTER TABLE jobs ADD COLUMN continue_parent_on_failure INTEGER NOT NULL DEFAULT 0;
272
+ `,
273
+ 26: `
274
+ ALTER TABLE jobs ADD COLUMN ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
275
+ `,
276
+ 27: `
277
+ CREATE TABLE IF NOT EXISTS flow_failures (
278
+ parent_id TEXT NOT NULL,
279
+ child_id TEXT NOT NULL,
280
+ child_queue TEXT NOT NULL,
281
+ mode TEXT NOT NULL,
282
+ error TEXT NOT NULL,
283
+ created_at INTEGER NOT NULL,
284
+ PRIMARY KEY (parent_id, child_id)
285
+ );
286
+ CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
287
+
288
+ DROP INDEX IF EXISTS idx_jobs_run_at;
289
+ CREATE INDEX idx_jobs_run_at
290
+ ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
291
+
292
+ DROP INDEX IF EXISTS idx_jobs_pending_priority;
293
+ CREATE INDEX idx_jobs_pending_priority
294
+ ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
295
+ `,
296
+ 28: DEPENDENCY_COMPLETION_SCHEMA,
297
+ 29: 'ALTER TABLE dependency_completions ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;',
244
298
  };
@@ -4,9 +4,11 @@
4
4
  * Uses MessagePack for ~2-3x faster serialization than JSON
5
5
  */
6
6
  import type { Job, JobId, JobTimelineEntry } from '../../domain/types/job';
7
+ import type { FlowFailureRecord } from '../../domain/types/flow';
7
8
  import type { StallConfig } from '../../domain/types/stall';
8
9
  import type { CronJob } from '../../domain/types/cron';
9
10
  import { type DlqConfig, type DlqEntry } from '../../domain/types/dlq';
11
+ import { type DependencyCompletionRecord } from './dependencyCompletionStore';
10
12
  /** Critical-loss callback: invoked when WriteBuffer drops jobs after exhausting retries. */
11
13
  export type SqliteCriticalLossCallback = (jobs: Job[], lastError: Error, attempts: number) => void;
12
14
  /** Record of a critical job loss event (jobs dropped after max retries). */
@@ -37,6 +39,7 @@ export declare class SqliteStorage {
37
39
  private readonly statements;
38
40
  private readonly batchManager;
39
41
  private readonly writeBuffer;
42
+ private readonly dependencyCompletionStore;
40
43
  private _diskFull;
41
44
  private _lastDiskFullError;
42
45
  private _lastDiskFullAt;
@@ -98,6 +101,11 @@ export declare class SqliteStorage {
98
101
  markFailed(job: Job, error: string | null): void;
99
102
  /** Save DLQ entry with full metadata */
100
103
  saveDlqEntry(entry: DlqEntry): void;
104
+ /**
105
+ * Commit a terminal child outcome, its optional DLQ row, and its flow outbox
106
+ * record together. Recovery therefore never observes only half the transition.
107
+ */
108
+ commitFailedJob(jobId: JobId, entry: DlqEntry | null, flowFailure: FlowFailureRecord | null): void;
101
109
  /** Delete DLQ entry by job ID */
102
110
  deleteDlqEntry(jobId: JobId): void;
103
111
  /** Clear all DLQ entries for a queue */
@@ -129,6 +137,25 @@ export declare class SqliteStorage {
129
137
  updateRunAt(jobId: JobId, runAt: number): void;
130
138
  /** Update a job's children_ids blob and parent_id */
131
139
  updateJobChildrenIds(jobId: JobId, childrenIds: JobId[]): void;
140
+ /** Persist both sides of a legacy parent link in one SQLite transaction. */
141
+ updateFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data'>, parentState: 'waiting-children' | 'waiting' | 'prioritized' | 'delayed'): void;
142
+ /**
143
+ * Persist a legacy SDK parent-id backpatch without changing parent state.
144
+ *
145
+ * A fast child can finish before the SDK sends UpdateParent. Completed jobs
146
+ * remain in `jobs`, while failed jobs live only in the serialized DLQ entry.
147
+ * The failure outbox is re-keyed in the same transaction so a crash between
148
+ * this write and in-memory propagation still targets the real parent.
149
+ */
150
+ backpatchFlowChild(child: Pick<Job, 'id' | 'parentId' | 'data'>, previousParentId: JobId | null): void;
151
+ /** Persist both sides of a removed parent/child relationship atomically. */
152
+ removeFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data' | 'runAt'>, parentState: 'waiting-children' | 'waiting' | 'prioritized'): void;
153
+ /** Record terminal child propagation before applying its parent-side effect. */
154
+ saveFlowFailure(record: FlowFailureRecord): void;
155
+ loadFlowFailures(): FlowFailureRecord[];
156
+ deleteFlowFailure(parentId: JobId, childId?: JobId): void;
157
+ /** Persist dependency removal/promotion as one parent-side recovery checkpoint. */
158
+ updateFlowParentResolution(job: Pick<Job, 'id' | 'dependsOn' | 'runAt' | 'priority' | 'timeline'>, stateOverride?: 'waiting-children' | 'waiting' | 'prioritized' | 'delayed'): void;
132
159
  getJob(id: JobId): Job | null;
133
160
  storeResult(jobId: JobId, result: unknown): void;
134
161
  getResult(jobId: JobId): unknown;
@@ -144,6 +171,14 @@ export declare class SqliteStorage {
144
171
  getJobStateRaw(jobId: JobId): string | null;
145
172
  /** Load all completed job IDs (for dependency recovery) */
146
173
  loadCompletedJobIds(): Set<JobId>;
174
+ /** Atomically replace a removed completed job with payload-free dependency evidence. */
175
+ commitRemovedCompletion(job: Pick<Job, 'id' | 'queue'>, retentionLimit: number, pinned: boolean, completedAt?: number): void;
176
+ loadDependencyCompletions(): DependencyCompletionRecord[];
177
+ pinDependencyCompletions(jobIds: Iterable<JobId>): void;
178
+ unpinDependencyCompletions(jobIds: Iterable<JobId>, retentionLimit: number): DependencyCompletionRecord[];
179
+ reconcileDependencyCompletionPins(referenced: ReadonlySet<JobId>, retentionLimit: number): DependencyCompletionRecord[];
180
+ deleteDependencyCompletion(jobId: JobId): boolean;
181
+ deleteDependencyCompletionsForQueue(queue: string): JobId[];
147
182
  /**
148
183
  * Insert batch of jobs. By default the jobs go through the write buffer.
149
184
  * When `durable` is true they bypass the buffer and are written to disk
@@ -7,9 +7,10 @@ import { Database } from 'bun:sqlite';
7
7
  import { createDlqEntry, } from '../../domain/types/dlq';
8
8
  import { PRAGMA_SETTINGS, SCHEMA, MIGRATION_TABLE, SCHEMA_VERSION, MIGRATIONS } from './schema';
9
9
  import { prepareStatements, } from './statements';
10
- import { pack, persistedStallCount, reconstructDlqEntry, rowToJob, unpack, } from './sqliteSerializer';
10
+ import { pack, persistedInitialState, persistedStallCount, reconstructDlqEntry, rowToJob, unpack, } from './sqliteSerializer';
11
11
  import { BatchInsertManager, WriteBuffer } from './sqliteBatch';
12
12
  import { storageLog } from '../../shared/logger';
13
+ import { DependencyCompletionStore, } from './dependencyCompletionStore';
13
14
  /** Check if an error is a SQLITE_FULL (disk full) error */
14
15
  function isSqliteFullError(err) {
15
16
  if (!(err instanceof Error))
@@ -25,6 +26,7 @@ export class SqliteStorage {
25
26
  statements;
26
27
  batchManager;
27
28
  writeBuffer;
29
+ dependencyCompletionStore;
28
30
  _diskFull = false;
29
31
  _lastDiskFullError = null;
30
32
  _lastDiskFullAt = null;
@@ -48,6 +50,7 @@ export class SqliteStorage {
48
50
  });
49
51
  }
50
52
  this.migrate();
53
+ this.dependencyCompletionStore = new DependencyCompletionStore(this.db);
51
54
  this.statements = prepareStatements(this.db);
52
55
  this._onCriticalLoss = config.onCriticalLoss;
53
56
  // Initialize batch manager and write buffer
@@ -256,7 +259,7 @@ export class SqliteStorage {
256
259
  runInsertJobStmt(job) {
257
260
  this.statements
258
261
  .get('insertJob')
259
- .run(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, job.runAt > Date.now() ? 'delayed' : 'waiting', job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
262
+ .run(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, persistedInitialState(job), job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.failParentOnFailure ? 1 : 0, job.removeDependencyOnFailure ? 1 : 0, job.continueParentOnFailure ? 1 : 0, job.ignoreDependencyOnFailure ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
260
263
  }
261
264
  /**
262
265
  * Ensure a job's buffered INSERT has been written to disk before issuing a
@@ -308,6 +311,35 @@ export class SqliteStorage {
308
311
  .run(entry.job.id, entry.job.queue, pack(entry), entry.enteredAt);
309
312
  });
310
313
  }
314
+ /**
315
+ * Commit a terminal child outcome, its optional DLQ row, and its flow outbox
316
+ * record together. Recovery therefore never observes only half the transition.
317
+ */
318
+ commitFailedJob(jobId, entry, flowFailure) {
319
+ this.writeBuffer.removePending(jobId);
320
+ this.safeWrite(() => {
321
+ const tx = this.db.transaction(() => {
322
+ if (entry) {
323
+ this.statements
324
+ .get('insertDlq')
325
+ .run(entry.job.id, entry.job.queue, pack(entry), entry.enteredAt);
326
+ }
327
+ if (flowFailure) {
328
+ this.db
329
+ .prepare(`INSERT INTO flow_failures
330
+ (parent_id, child_id, child_queue, mode, error, created_at)
331
+ VALUES (?, ?, ?, ?, ?, ?)
332
+ ON CONFLICT(parent_id, child_id) DO UPDATE SET
333
+ child_queue=excluded.child_queue, mode=excluded.mode,
334
+ error=excluded.error, created_at=excluded.created_at`)
335
+ .run(flowFailure.parentId, flowFailure.childId, flowFailure.childQueue, flowFailure.mode, flowFailure.error, flowFailure.createdAt);
336
+ }
337
+ this.statements.get('deleteJob').run(jobId);
338
+ this.statements.get('deleteJobResult').run(jobId);
339
+ });
340
+ tx();
341
+ });
342
+ }
311
343
  /** Delete DLQ entry by job ID */
312
344
  deleteDlqEntry(jobId) {
313
345
  this.safeWrite(() => {
@@ -388,6 +420,7 @@ export class SqliteStorage {
388
420
  const tx = this.db.transaction((id) => {
389
421
  deleteStmt.run(id);
390
422
  deleteResultStmt.run(id);
423
+ this.db.prepare('DELETE FROM flow_failures WHERE parent_id = ?').run(id);
391
424
  });
392
425
  tx(jobId);
393
426
  });
@@ -438,12 +471,161 @@ export class SqliteStorage {
438
471
  }
439
472
  /** Update a job's children_ids blob and parent_id */
440
473
  updateJobChildrenIds(jobId, childrenIds) {
474
+ this.flushIfBuffered(jobId);
441
475
  this.safeWrite(() => {
442
476
  this.db
443
477
  .prepare('UPDATE jobs SET children_ids = ? WHERE id = ?')
444
478
  .run(childrenIds.length > 0 ? pack(childrenIds) : null, jobId);
445
479
  });
446
480
  }
481
+ /** Persist both sides of a legacy parent link in one SQLite transaction. */
482
+ updateFlowLink(child, parent, parentState) {
483
+ this.flushIfBuffered(child.id);
484
+ this.flushIfBuffered(parent.id);
485
+ this.safeWrite(() => {
486
+ const tx = this.db.transaction(() => {
487
+ this.db
488
+ .prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
489
+ .run(child.parentId, pack(child.data), child.id);
490
+ this.db
491
+ .prepare('UPDATE jobs SET children_ids = ?, depends_on = ?, data = ?, state = ? WHERE id = ?')
492
+ .run(parent.childrenIds.length > 0 ? pack(parent.childrenIds) : null, parent.dependsOn.length > 0 ? pack(parent.dependsOn) : null, pack(parent.data), parentState, parent.id);
493
+ });
494
+ tx();
495
+ });
496
+ }
497
+ /**
498
+ * Persist a legacy SDK parent-id backpatch without changing parent state.
499
+ *
500
+ * A fast child can finish before the SDK sends UpdateParent. Completed jobs
501
+ * remain in `jobs`, while failed jobs live only in the serialized DLQ entry.
502
+ * The failure outbox is re-keyed in the same transaction so a crash between
503
+ * this write and in-memory propagation still targets the real parent.
504
+ */
505
+ backpatchFlowChild(child, previousParentId) {
506
+ this.flushIfBuffered(child.id);
507
+ this.safeWrite(() => {
508
+ const tx = this.db.transaction(() => {
509
+ let persisted = this.db
510
+ .prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
511
+ .run(child.parentId, pack(child.data), child.id).changes > 0;
512
+ const dlqRows = this.db
513
+ .query('SELECT id, entry FROM dlq WHERE job_id = ?')
514
+ .all(String(child.id));
515
+ for (const row of dlqRows) {
516
+ const entry = unpack(row.entry, null, `backpatchFlowChild:${String(child.id)}`);
517
+ if (!entry?.job)
518
+ continue;
519
+ const updated = {
520
+ ...entry,
521
+ job: { ...entry.job, parentId: child.parentId, data: child.data },
522
+ };
523
+ this.db.prepare('UPDATE dlq SET entry = ? WHERE id = ?').run(pack(updated), row.id);
524
+ persisted = true;
525
+ }
526
+ if (!persisted) {
527
+ throw new Error(`Flow child is no longer persisted: ${String(child.id)}`);
528
+ }
529
+ if (previousParentId && child.parentId && previousParentId !== child.parentId) {
530
+ const failure = this.db
531
+ .query(`SELECT child_queue, mode, error, created_at
532
+ FROM flow_failures
533
+ WHERE parent_id = ? AND child_id = ?`)
534
+ .get(String(previousParentId), String(child.id));
535
+ if (failure) {
536
+ this.db
537
+ .prepare(`INSERT INTO flow_failures
538
+ (parent_id, child_id, child_queue, mode, error, created_at)
539
+ VALUES (?, ?, ?, ?, ?, ?)
540
+ ON CONFLICT(parent_id, child_id) DO UPDATE SET
541
+ child_queue=excluded.child_queue, mode=excluded.mode,
542
+ error=excluded.error, created_at=excluded.created_at`)
543
+ .run(child.parentId, child.id, failure.child_queue, failure.mode, failure.error, failure.created_at);
544
+ this.db
545
+ .prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
546
+ .run(previousParentId, child.id);
547
+ }
548
+ }
549
+ });
550
+ tx();
551
+ });
552
+ }
553
+ /** Persist both sides of a removed parent/child relationship atomically. */
554
+ removeFlowLink(child, parent, parentState) {
555
+ this.flushIfBuffered(child.id);
556
+ this.flushIfBuffered(parent.id);
557
+ this.safeWrite(() => {
558
+ const tx = this.db.transaction(() => {
559
+ this.db
560
+ .prepare('UPDATE jobs SET parent_id = ?, data = ? WHERE id = ?')
561
+ .run(child.parentId, pack(child.data), child.id);
562
+ this.db
563
+ .prepare(`UPDATE jobs
564
+ SET children_ids = ?, depends_on = ?, data = ?, run_at = ?, state = ?
565
+ WHERE id = ?`)
566
+ .run(parent.childrenIds.length > 0 ? pack(parent.childrenIds) : null, parent.dependsOn.length > 0 ? pack(parent.dependsOn) : null, pack(parent.data), parent.runAt, parentState, parent.id);
567
+ this.db
568
+ .prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
569
+ .run(parent.id, child.id);
570
+ });
571
+ tx();
572
+ });
573
+ }
574
+ /** Record terminal child propagation before applying its parent-side effect. */
575
+ saveFlowFailure(record) {
576
+ this.safeWrite(() => {
577
+ this.db
578
+ .prepare(`INSERT INTO flow_failures
579
+ (parent_id, child_id, child_queue, mode, error, created_at)
580
+ VALUES (?, ?, ?, ?, ?, ?)
581
+ ON CONFLICT(parent_id, child_id) DO UPDATE SET
582
+ child_queue=excluded.child_queue, mode=excluded.mode,
583
+ error=excluded.error, created_at=excluded.created_at`)
584
+ .run(record.parentId, record.childId, record.childQueue, record.mode, record.error, record.createdAt);
585
+ });
586
+ }
587
+ loadFlowFailures() {
588
+ const rows = this.db
589
+ .query('SELECT * FROM flow_failures ORDER BY created_at, parent_id, child_id')
590
+ .all();
591
+ return rows.map((row) => ({
592
+ parentId: row.parent_id,
593
+ childId: row.child_id,
594
+ childQueue: row.child_queue,
595
+ mode: row.mode,
596
+ error: row.error,
597
+ createdAt: row.created_at,
598
+ }));
599
+ }
600
+ deleteFlowFailure(parentId, childId) {
601
+ this.safeWrite(() => {
602
+ if (childId) {
603
+ this.db
604
+ .prepare('DELETE FROM flow_failures WHERE parent_id = ? AND child_id = ?')
605
+ .run(parentId, childId);
606
+ }
607
+ else {
608
+ this.db.prepare('DELETE FROM flow_failures WHERE parent_id = ?').run(parentId);
609
+ }
610
+ });
611
+ }
612
+ /** Persist dependency removal/promotion as one parent-side recovery checkpoint. */
613
+ updateFlowParentResolution(job, stateOverride) {
614
+ this.flushIfBuffered(job.id);
615
+ const state = stateOverride ??
616
+ (job.dependsOn.length > 0
617
+ ? 'waiting-children'
618
+ : job.runAt > Date.now()
619
+ ? 'delayed'
620
+ : job.priority > 0
621
+ ? 'prioritized'
622
+ : 'waiting');
623
+ this.safeWrite(() => {
624
+ this.db
625
+ .prepare('UPDATE jobs SET depends_on = ?, run_at = ?, state = ?, timeline = ? WHERE id = ?')
626
+ .run(job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.runAt, state, job.timeline.length > 0 ? pack(job.timeline) : null, job.id);
627
+ });
628
+ }
447
629
  getJob(id) {
448
630
  const row = this.statements.get('getJob').get(id);
449
631
  return row ? rowToJob(row) : null;
@@ -495,18 +677,62 @@ export class SqliteStorage {
495
677
  }
496
678
  /** Load all completed job IDs (for dependency recovery) */
497
679
  loadCompletedJobIds() {
498
- const rows = this.db.query('SELECT job_id FROM job_results').all();
499
- const ids = new Set(rows.map((r) => r.job_id));
500
- // A job acked with no/undefined result has state='completed' but NO job_results
501
- // row. Include state='completed' ids so dependency recovery still sees it as
502
- // done and unblocks dependents (instead of parking them forever).
680
+ // `jobs.state` is the completion authority. A result row can exist while its
681
+ // job is still active if a process dies between the two legacy writes, so it
682
+ // must never release a dependency by itself.
683
+ const ids = new Set();
503
684
  const stateRows = this.db
504
685
  .query("SELECT id FROM jobs WHERE state = 'completed'")
505
686
  .all();
506
687
  for (const r of stateRows)
507
688
  ids.add(r.id);
689
+ for (const record of this.dependencyCompletionStore.load())
690
+ ids.add(record.jobId);
508
691
  return ids;
509
692
  }
693
+ /** Atomically replace a removed completed job with payload-free dependency evidence. */
694
+ commitRemovedCompletion(job, retentionLimit, pinned, completedAt = Date.now()) {
695
+ this.writeBuffer.removePending(job.id);
696
+ this.safeWrite(() => {
697
+ this.dependencyCompletionStore.commit({ jobId: job.id, queue: job.queue, completedAt, pinned }, retentionLimit);
698
+ });
699
+ }
700
+ loadDependencyCompletions() {
701
+ return this.dependencyCompletionStore.load();
702
+ }
703
+ pinDependencyCompletions(jobIds) {
704
+ this.safeWrite(() => {
705
+ this.dependencyCompletionStore.pin(jobIds);
706
+ });
707
+ }
708
+ unpinDependencyCompletions(jobIds, retentionLimit) {
709
+ let records = [];
710
+ this.safeWrite(() => {
711
+ records = this.dependencyCompletionStore.unpin(jobIds, retentionLimit);
712
+ });
713
+ return records;
714
+ }
715
+ reconcileDependencyCompletionPins(referenced, retentionLimit) {
716
+ let records = [];
717
+ this.safeWrite(() => {
718
+ records = this.dependencyCompletionStore.reconcilePins(referenced, retentionLimit);
719
+ });
720
+ return records;
721
+ }
722
+ deleteDependencyCompletion(jobId) {
723
+ let deleted = false;
724
+ this.safeWrite(() => {
725
+ deleted = this.dependencyCompletionStore.delete(jobId);
726
+ });
727
+ return deleted;
728
+ }
729
+ deleteDependencyCompletionsForQueue(queue) {
730
+ let deleted = [];
731
+ this.safeWrite(() => {
732
+ deleted = this.dependencyCompletionStore.deleteForQueue(queue);
733
+ });
734
+ return deleted;
735
+ }
510
736
  // ============ Bulk Operations ============
511
737
  /**
512
738
  * Insert batch of jobs. By default the jobs go through the write buffer.
@@ -573,15 +799,15 @@ export class SqliteStorage {
573
799
  const predicateParams = [];
574
800
  const requested = new Set(uniqueStates);
575
801
  if (requested.has('waiting')) {
576
- predicates.push("(state IN ('waiting', 'delayed') AND run_at <= ? AND priority <= 0)");
802
+ predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at <= ? AND priority <= 0)");
577
803
  predicateParams.push(options.now);
578
804
  }
579
805
  if (requested.has('prioritized')) {
580
- predicates.push("(state IN ('waiting', 'delayed') AND run_at <= ? AND priority > 0)");
806
+ predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at <= ? AND priority > 0)");
581
807
  predicateParams.push(options.now);
582
808
  }
583
809
  if (requested.has('delayed')) {
584
- predicates.push("(state IN ('waiting', 'delayed') AND run_at > ?)");
810
+ predicates.push("(state IN ('waiting', 'prioritized', 'delayed') AND run_at > ?)");
585
811
  predicateParams.push(options.now);
586
812
  }
587
813
  const persistedStates = uniqueStates.filter((state) => state !== 'waiting' && state !== 'prioritized' && state !== 'delayed');
@@ -607,7 +833,7 @@ export class SqliteStorage {
607
833
  */
608
834
  loadPendingJobs(limit = 10000, offset = 0) {
609
835
  const rows = this.db
610
- .query("SELECT * FROM jobs WHERE state IN ('waiting', 'delayed') ORDER BY priority DESC, run_at ASC, id ASC LIMIT ? OFFSET ?")
836
+ .query("SELECT * FROM jobs WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed') ORDER BY priority DESC, run_at ASC, id ASC LIMIT ? OFFSET ?")
611
837
  .all(limit, offset);
612
838
  return rows.map((row) => rowToJob(row));
613
839
  }
@@ -639,7 +865,7 @@ export class SqliteStorage {
639
865
  */
640
866
  countPendingJobs() {
641
867
  const result = this.db
642
- .query("SELECT COUNT(*) as count FROM jobs WHERE state IN ('waiting', 'delayed')")
868
+ .query("SELECT COUNT(*) as count FROM jobs WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed')")
643
869
  .get();
644
870
  return result?.count ?? 0;
645
871
  }
@@ -2,8 +2,8 @@
2
2
  * SQLite Batch Operations
3
3
  * High-performance batch insert with prepared statement caching
4
4
  */
5
- import { pack, persistedStallCount } from './sqliteSerializer';
6
- const COLS_PER_ROW = 25;
5
+ import { pack, persistedInitialState, persistedStallCount } from './sqliteSerializer';
6
+ const COLS_PER_ROW = 29;
7
7
  // SQLite has a limit of ~999 variables, so batch in chunks
8
8
  const MAX_ROWS_PER_INSERT = Math.floor(999 / COLS_PER_ROW);
9
9
  /**
@@ -75,7 +75,7 @@ export class BatchInsertManager {
75
75
  getBatchInsertStmt(size) {
76
76
  let stmt = this.cache.get(size);
77
77
  if (!stmt) {
78
- const rowPlaceholder = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
78
+ const rowPlaceholder = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
79
79
  const placeholders = Array(size).fill(rowPlaceholder).join(', ');
80
80
  // ON CONFLICT(id) DO UPDATE (upsert): a colliding id overwrites in place instead
81
81
  // of failing the whole flush. Without it, one stale ORPHAN row (left when
@@ -87,6 +87,8 @@ export class BatchInsertManager {
87
87
  id, queue, data, priority, created_at, run_at, attempts, max_attempts,
88
88
  backoff, ttl, timeout, unique_key, custom_id, depends_on, parent_id,
89
89
  children_ids, tags, state, lifo, group_id, remove_on_complete, remove_on_fail,
90
+ fail_parent_on_failure, remove_dependency_on_failure,
91
+ continue_parent_on_failure, ignore_dependency_on_failure,
90
92
  stall_timeout, stall_count, timeline
91
93
  ) VALUES ${placeholders}
92
94
  ON CONFLICT(id) DO UPDATE SET
@@ -97,6 +99,10 @@ export class BatchInsertManager {
97
99
  depends_on=excluded.depends_on, parent_id=excluded.parent_id, children_ids=excluded.children_ids,
98
100
  tags=excluded.tags, state=excluded.state, lifo=excluded.lifo, group_id=excluded.group_id,
99
101
  remove_on_complete=excluded.remove_on_complete, remove_on_fail=excluded.remove_on_fail,
102
+ fail_parent_on_failure=excluded.fail_parent_on_failure,
103
+ remove_dependency_on_failure=excluded.remove_dependency_on_failure,
104
+ continue_parent_on_failure=excluded.continue_parent_on_failure,
105
+ ignore_dependency_on_failure=excluded.ignore_dependency_on_failure,
100
106
  stall_timeout=excluded.stall_timeout, stall_count=excluded.stall_count,
101
107
  timeline=excluded.timeline,
102
108
  started_at=excluded.started_at, completed_at=excluded.completed_at,
@@ -116,7 +122,7 @@ export class BatchInsertManager {
116
122
  // Flatten all values
117
123
  const values = [];
118
124
  for (const job of jobs) {
119
- values.push(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, job.runAt > now ? 'delayed' : 'waiting', job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
125
+ values.push(job.id, job.queue, pack(job.data), job.priority, job.createdAt, job.runAt, job.attempts, job.maxAttempts, job.backoff, job.ttl, job.timeout, job.uniqueKey, job.customId, job.dependsOn.length > 0 ? pack(job.dependsOn) : null, job.parentId, job.childrenIds.length > 0 ? pack(job.childrenIds) : null, job.tags.length > 0 ? pack(job.tags) : null, persistedInitialState(job, now), job.lifo ? 1 : 0, job.groupId, job.removeOnComplete ? 1 : 0, job.removeOnFail ? 1 : 0, job.failParentOnFailure ? 1 : 0, job.removeDependencyOnFailure ? 1 : 0, job.continueParentOnFailure ? 1 : 0, job.ignoreDependencyOnFailure ? 1 : 0, job.stallTimeout, persistedStallCount(job), job.timeline.length > 0 ? pack(job.timeline) : null);
120
126
  }
121
127
  stmt.run(...values);
122
128
  }
@@ -16,6 +16,8 @@ export declare function unpack<T>(buffer: Uint8Array | null, fallback: T, contex
16
16
  * field receives the same zero default as a freshly created Job.
17
17
  */
18
18
  export declare function persistedStallCount(job: Pick<Job, 'stallCount'>): number;
19
+ /** Derive the durable initial state from the authoritative timeline. */
20
+ export declare function persistedInitialState(job: Job, now?: number): string;
19
21
  /**
20
22
  * Symbol marker stamped on a Job whose `depends_on` blob failed to decode.
21
23
  *
@@ -30,6 +32,9 @@ export declare function persistedStallCount(job: Pick<Job, 'stallCount'>): numbe
30
32
  * so it only exists on the in-memory Job for the duration of recovery.
31
33
  */
32
34
  export declare const CORRUPT_DEPENDS_ON: unique symbol;
35
+ export type PersistedJobState = 'active' | 'completed' | 'delayed' | 'prioritized' | 'waiting' | 'waiting-children';
36
+ /** Read the authoritative SQLite state carried by a recovered job. */
37
+ export declare function persistedJobState(job: Job): PersistedJobState | undefined;
33
38
  /** True if a Job was recovered with a corrupt `depends_on` blob. */
34
39
  export declare function isCorruptDependsOn(job: Job): boolean;
35
40
  /** Convert database row to Job object */
@@ -30,6 +30,19 @@ export function unpack(buffer, fallback, context) {
30
30
  export function persistedStallCount(job) {
31
31
  return job.stallCount ?? 0;
32
32
  }
33
+ /** Derive the durable initial state from the authoritative timeline. */
34
+ export function persistedInitialState(job, now = Date.now()) {
35
+ const latest = job.timeline[job.timeline.length - 1]?.state;
36
+ if (latest === 'waiting-children' ||
37
+ latest === 'waiting' ||
38
+ latest === 'prioritized' ||
39
+ latest === 'delayed') {
40
+ return latest;
41
+ }
42
+ if (job.runAt > now)
43
+ return 'delayed';
44
+ return job.priority > 0 ? 'prioritized' : 'waiting';
45
+ }
33
46
  /**
34
47
  * Symbol marker stamped on a Job whose `depends_on` blob failed to decode.
35
48
  *
@@ -44,6 +57,11 @@ export function persistedStallCount(job) {
44
57
  * so it only exists on the in-memory Job for the duration of recovery.
45
58
  */
46
59
  export const CORRUPT_DEPENDS_ON = Symbol('bunqueue.corruptDependsOn');
60
+ const PERSISTED_JOB_STATE = Symbol('bunqueue.persistedJobState');
61
+ /** Read the authoritative SQLite state carried by a recovered job. */
62
+ export function persistedJobState(job) {
63
+ return job[PERSISTED_JOB_STATE];
64
+ }
47
65
  /** True if a Job was recovered with a corrupt `depends_on` blob. */
48
66
  export function isCorruptDependsOn(job) {
49
67
  return job[CORRUPT_DEPENDS_ON] === true;
@@ -113,14 +131,14 @@ export function rowToJob(row) {
113
131
  lastHeartbeat: row.last_heartbeat ?? row.created_at,
114
132
  stallTimeout: row.stall_timeout,
115
133
  stallCount: row.stall_count ?? 0,
116
- // BullMQ v5 additional fields (not persisted in DB, use defaults)
134
+ // BullMQ v5 additional fields
117
135
  stackTraceLimit: 10,
118
136
  keepLogs: null,
119
137
  sizeLimit: null,
120
- failParentOnFailure: false,
121
- removeDependencyOnFailure: false,
122
- continueParentOnFailure: false,
123
- ignoreDependencyOnFailure: false,
138
+ failParentOnFailure: row.fail_parent_on_failure === 1,
139
+ removeDependencyOnFailure: row.remove_dependency_on_failure === 1,
140
+ continueParentOnFailure: row.continue_parent_on_failure === 1,
141
+ ignoreDependencyOnFailure: row.ignore_dependency_on_failure === 1,
124
142
  deduplicationTtl: null,
125
143
  deduplicationExtend: false,
126
144
  deduplicationReplace: false,
@@ -133,6 +151,11 @@ export function rowToJob(row) {
133
151
  ? unpack(row.stacktrace, null, `${jobContext}:stacktrace`)
134
152
  : null,
135
153
  };
154
+ Object.defineProperty(job, PERSISTED_JOB_STATE, {
155
+ value: row.state,
156
+ enumerable: false,
157
+ configurable: false,
158
+ });
136
159
  // Stamp a collision-proof corruption marker (non-enumerable Symbol, never
137
160
  // persisted) so the recovery path routes this job to the DLQ rather than
138
161
  // enqueuing it as ready. We keep dependsOn: [] here — the real deps are
@@ -37,6 +37,10 @@ export interface DbJob {
37
37
  progress_msg: string | null;
38
38
  remove_on_complete: number;
39
39
  remove_on_fail: number;
40
+ fail_parent_on_failure: number;
41
+ remove_dependency_on_failure: number;
42
+ continue_parent_on_failure: number;
43
+ ignore_dependency_on_failure: number;
40
44
  stall_timeout: number | null;
41
45
  last_heartbeat: number | null;
42
46
  stall_count: number;