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.
- package/README.md +62 -9
- package/dist/application/backgroundTasks.js +21 -4
- package/dist/application/cleanupTasks.js +7 -3
- package/dist/application/contextFactory.d.ts +6 -3
- package/dist/application/contextFactory.js +8 -0
- package/dist/application/dependencyCompletions.d.ts +53 -0
- package/dist/application/dependencyCompletions.js +123 -0
- package/dist/application/dependencyProcessor.d.ts +5 -0
- package/dist/application/dependencyProcessor.js +25 -13
- package/dist/application/flowFailureRecovery.d.ts +20 -0
- package/dist/application/flowFailureRecovery.js +94 -0
- package/dist/application/flowParentBackpatch.d.ts +27 -0
- package/dist/application/flowParentBackpatch.js +120 -0
- package/dist/application/operations/ack.d.ts +6 -3
- package/dist/application/operations/ack.js +36 -10
- package/dist/application/operations/ackHelpers.d.ts +5 -4
- package/dist/application/operations/ackHelpers.js +4 -10
- package/dist/application/operations/customId.d.ts +3 -0
- package/dist/application/operations/customId.js +10 -0
- package/dist/application/operations/flowPush.d.ts +9 -0
- package/dist/application/operations/flowPush.js +112 -0
- package/dist/application/operations/flowTopologyValidation.d.ts +3 -0
- package/dist/application/operations/flowTopologyValidation.js +101 -0
- package/dist/application/operations/flowValidation.d.ts +3 -0
- package/dist/application/operations/flowValidation.js +166 -0
- package/dist/application/operations/jobManagement.d.ts +3 -0
- package/dist/application/operations/jobManagement.js +10 -4
- package/dist/application/operations/push.d.ts +6 -1
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/pushInsert.d.ts +11 -2
- package/dist/application/operations/pushInsert.js +20 -8
- package/dist/application/operations/pushLocks.d.ts +1 -0
- package/dist/application/operations/pushLocks.js +5 -1
- package/dist/application/operations/queryOperations.js +7 -6
- package/dist/application/queueManager.d.ts +7 -0
- package/dist/application/queueManager.js +261 -67
- package/dist/application/types.d.ts +3 -1
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flow.d.ts +3 -6
- package/dist/client/flow.js +71 -210
- package/dist/client/flowAtomic.d.ts +9 -0
- package/dist/client/flowAtomic.js +24 -0
- package/dist/client/flowJobCoreMethods.d.ts +28 -0
- package/dist/client/flowJobCoreMethods.js +151 -0
- package/dist/client/flowJobDependencies.d.ts +7 -0
- package/dist/client/flowJobDependencies.js +64 -0
- package/dist/client/flowJobFactory.d.ts +11 -24
- package/dist/client/flowJobFactory.js +91 -360
- package/dist/client/flowJobMoveMethods.d.ts +16 -0
- package/dist/client/flowJobMoveMethods.js +112 -0
- package/dist/client/flowJobTypes.d.ts +29 -0
- package/dist/client/flowJobTypes.js +6 -0
- package/dist/client/flowLegacyPlan.d.ts +16 -0
- package/dist/client/flowLegacyPlan.js +118 -0
- package/dist/client/flowOptions.d.ts +10 -0
- package/dist/client/flowOptions.js +44 -0
- package/dist/client/flowPlan.d.ts +16 -0
- package/dist/client/flowPlan.js +101 -0
- package/dist/client/flowPush.js +4 -1
- package/dist/client/flowReader.d.ts +10 -0
- package/dist/client/flowReader.js +147 -0
- package/dist/client/flowTypes.d.ts +2 -2
- package/dist/client/jobHelpers.js +2 -0
- package/dist/client/workflow/clock.d.ts +3 -1
- package/dist/client/workflow/clock.js +18 -1
- package/dist/client/workflow/compensationChild.d.ts +30 -0
- package/dist/client/workflow/compensationChild.js +65 -0
- package/dist/client/workflow/compensationPass.d.ts +16 -0
- package/dist/client/workflow/compensationPass.js +96 -0
- package/dist/client/workflow/compensationSupport.d.ts +24 -0
- package/dist/client/workflow/compensationSupport.js +57 -0
- package/dist/client/workflow/compensator.d.ts +11 -50
- package/dist/client/workflow/compensator.js +22 -374
- package/dist/client/workflow/definitionGuard.d.ts +11 -0
- package/dist/client/workflow/definitionGuard.js +22 -0
- package/dist/client/workflow/engine.d.ts +2 -2
- package/dist/client/workflow/engine.js +8 -8
- package/dist/client/workflow/eventTypes.d.ts +44 -0
- package/dist/client/workflow/eventTypes.js +2 -0
- package/dist/client/workflow/executionTypes.d.ts +133 -0
- package/dist/client/workflow/executionTypes.js +2 -0
- package/dist/client/workflow/executor.d.ts +4 -17
- package/dist/client/workflow/executor.js +50 -208
- package/dist/client/workflow/executorLifecycle.d.ts +18 -0
- package/dist/client/workflow/executorLifecycle.js +69 -0
- package/dist/client/workflow/executorNodes.d.ts +17 -0
- package/dist/client/workflow/executorNodes.js +138 -0
- package/dist/client/workflow/identity.d.ts +2 -0
- package/dist/client/workflow/identity.js +8 -0
- package/dist/client/workflow/index.d.ts +1 -1
- package/dist/client/workflow/loops.d.ts +3 -5
- package/dist/client/workflow/loops.js +25 -23
- package/dist/client/workflow/mapRunner.d.ts +4 -0
- package/dist/client/workflow/mapRunner.js +45 -0
- package/dist/client/workflow/recovery.js +10 -3
- package/dist/client/workflow/runner.d.ts +2 -21
- package/dist/client/workflow/runner.js +39 -104
- package/dist/client/workflow/runnerTiming.d.ts +14 -0
- package/dist/client/workflow/runnerTiming.js +66 -0
- package/dist/client/workflow/stepTypes.d.ts +171 -0
- package/dist/client/workflow/stepTypes.js +3 -0
- package/dist/client/workflow/store.d.ts +17 -17
- package/dist/client/workflow/store.js +65 -100
- package/dist/client/workflow/storeExecutionCodec.d.ts +11 -0
- package/dist/client/workflow/storeExecutionCodec.js +34 -0
- package/dist/client/workflow/storeListing.d.ts +11 -0
- package/dist/client/workflow/storeListing.js +45 -0
- package/dist/client/workflow/storeMaintenance.d.ts +4 -0
- package/dist/client/workflow/storeMaintenance.js +40 -0
- package/dist/client/workflow/storeSignals.d.ts +9 -0
- package/dist/client/workflow/storeSignals.js +38 -2
- package/dist/client/workflow/subWorkflowRunner.d.ts +13 -0
- package/dist/client/workflow/subWorkflowRunner.js +40 -0
- package/dist/client/workflow/types.d.ts +4 -356
- package/dist/client/workflow/types.js +1 -3
- package/dist/client/workflow/waitFor.js +39 -26
- package/dist/client/workflow/workflow.d.ts +16 -59
- package/dist/client/workflow/workflow.js +53 -179
- package/dist/client/workflow/workflowDecisions.d.ts +11 -0
- package/dist/client/workflow/workflowDecisions.js +27 -0
- package/dist/client/workflow/workflowDefinition.d.ts +16 -0
- package/dist/client/workflow/workflowDefinition.js +123 -0
- package/dist/client/workflow/workflowIntrospection.d.ts +5 -0
- package/dist/client/workflow/workflowIntrospection.js +46 -0
- package/dist/client/workflow/workflowValidation.d.ts +44 -0
- package/dist/client/workflow/workflowValidation.js +143 -0
- package/dist/domain/types/command.d.ts +7 -1
- package/dist/domain/types/flow.d.ts +25 -0
- package/dist/domain/types/flow.js +1 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.d.ts +6 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.js +16 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.d.ts +38 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.js +105 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -5
- package/dist/infrastructure/persistence/schema.js +57 -3
- package/dist/infrastructure/persistence/sqlite.d.ts +35 -0
- package/dist/infrastructure/persistence/sqlite.js +238 -12
- package/dist/infrastructure/persistence/sqliteBatch.js +10 -4
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts +5 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +28 -5
- package/dist/infrastructure/persistence/statements.d.ts +4 -0
- package/dist/infrastructure/persistence/statements.js +8 -2
- package/dist/infrastructure/server/handlerRoutes.js +3 -0
- package/dist/infrastructure/server/handlers/advanced.js +2 -2
- package/dist/infrastructure/server/handlers/flow.d.ts +7 -0
- package/dist/infrastructure/server/handlers/flow.js +11 -0
- package/dist/infrastructure/server/handlers/index.d.ts +1 -0
- package/dist/infrastructure/server/handlers/index.js +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -103,7 +103,9 @@ Python, PHP, Go, Rust and Elixir clients speak the same protocol — see
|
|
|
103
103
|
- **BullMQ-compatible API** — same `Queue`, `Worker`, `QueueEvents`; [migrating takes minutes](https://bunqueue.dev/guide/migration/)
|
|
104
104
|
- **MCP server included** — 73 tools; AI agents get full queue control out of the box
|
|
105
105
|
- **Everything server-side** — retries with backoff, priorities, cron, rate limits, dead letter queue
|
|
106
|
-
- **
|
|
106
|
+
- **Measured, operation-specific performance** — 729K jobs/sec internal
|
|
107
|
+
in-memory batch push, 186K jobs/sec public on-disk Embedded `addBulk`, and
|
|
108
|
+
159K jobs/sec TCP `PUSHB`; [methodology and distributions](https://bunqueue.dev/guide/benchmarks/)
|
|
107
109
|
|
|
108
110
|
**Great for:** single-server deployments, AI agents that need a scheduler,
|
|
109
111
|
prototypes and MVPs, embedded use cases (CLI tools, edge, serverless), teams
|
|
@@ -120,7 +122,7 @@ failover today. If you already run Redis and BullMQ works for you, keep it.
|
|
|
120
122
|
| ---------------- | ------------------------------------- | -------------------------------------------- |
|
|
121
123
|
| **How it works** | Queue runs inside your process | Standalone server, clients connect via TCP |
|
|
122
124
|
| **Setup** | `bun add bunqueue` | `docker run` or `bunqueue start` |
|
|
123
|
-
| **Performance** |
|
|
125
|
+
| **Performance** | 186K jobs/sec on-disk `addBulk`; 729K internal in-memory batch | 159K jobs/sec TCP `PUSHB`; 17K jobs/sec worker drain |
|
|
124
126
|
| **Best for** | Single-process apps, CLIs, serverless | Multiple workers, separate producer/consumer |
|
|
125
127
|
| **Scaling** | Same process only | Multiple clients across machines |
|
|
126
128
|
|
|
@@ -210,6 +212,49 @@ Worker("emails", lambda job: {"sent": True}, concurrency=10).run()
|
|
|
210
212
|
Every SDK is certified against the same public
|
|
211
213
|
[wire protocol](https://github.com/egeominotti/bunqueue/blob/main/docs/protocol.md) and conformance suite.
|
|
212
214
|
|
|
215
|
+
### Atomic flows, in every SDK
|
|
216
|
+
|
|
217
|
+
Every official `FlowProducer` resolves all job IDs and reciprocal dependency
|
|
218
|
+
edges locally, then sends one `PUSHF` command. The broker validates the complete
|
|
219
|
+
graph and commits it atomically, so a worker cannot observe a leaf from a
|
|
220
|
+
partially-created flow.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { FlowProducer } from 'bunqueue-client';
|
|
224
|
+
|
|
225
|
+
const flows = new FlowProducer({ host: 'localhost', port: 6789 });
|
|
226
|
+
const root = await flows.add({
|
|
227
|
+
name: 'publish-release',
|
|
228
|
+
queueName: 'release',
|
|
229
|
+
data: { version: 'candidate-42' },
|
|
230
|
+
children: [
|
|
231
|
+
{ name: 'unit-tests', queueName: 'checks', data: { suite: 'unit' } },
|
|
232
|
+
{ name: 'sdk-tests', queueName: 'checks', data: { suite: 'sdk' } },
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log(root.job.id, root.children?.map(({ job }) => job.id));
|
|
237
|
+
await flows.close();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The repository records the contracts and the test strategy beside each
|
|
241
|
+
implementation:
|
|
242
|
+
|
|
243
|
+
| SDK | Runtime invariants | Generated tests | Mutation engine |
|
|
244
|
+
| --- | --- | --- | --- |
|
|
245
|
+
| [TypeScript](./sdk/typescript/README.md) | [contract](./sdk/typescript/INVARIANTS.md) | fast-check | StrykerJS |
|
|
246
|
+
| [Python](./sdk/python/README.md) | [contract](./sdk/python/INVARIANTS.md) | Hypothesis | mutmut |
|
|
247
|
+
| [PHP](./sdk/php/README.md) | [contract](./sdk/php/INVARIANTS.md) | Eris | Infection |
|
|
248
|
+
| [Go](./sdk/go/README.md) | [contract](./sdk/go/INVARIANTS.md) | Rapid | Gremlins |
|
|
249
|
+
| [Rust](./sdk/rust/README.md) | [contract](./sdk/rust/INVARIANTS.md) | proptest | cargo-mutants |
|
|
250
|
+
| [Elixir](./sdk/elixir/README.md) | [contract](./sdk/elixir/INVARIANTS.md) | StreamData | Muex |
|
|
251
|
+
|
|
252
|
+
Property campaigns run in the ordinary SDK gate with deterministic replay
|
|
253
|
+
seeds. Mutation campaigns run separately against the pure planners and
|
|
254
|
+
snapshot validators. Contributors can reproduce the complete isolated SDK
|
|
255
|
+
gate with `bun run test:sandbox:sdk`; language-specific commands live in each
|
|
256
|
+
SDK README and `AGENTS.md`.
|
|
257
|
+
|
|
213
258
|
[SDK guide (all six languages) →](https://bunqueue.dev/guide/sdks/)
|
|
214
259
|
|
|
215
260
|
## Simple Mode
|
|
@@ -343,13 +388,21 @@ https://github.com/user-attachments/assets/e8a8d38e-b4a6-4dc8-8360-876c0f24d116
|
|
|
343
388
|
|
|
344
389
|
## Performance
|
|
345
390
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
|
349
|
-
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
391
|
+
Native Ryzen 9 9950X3D, Bun 1.3.14; medians from repeated fresh processes:
|
|
392
|
+
|
|
393
|
+
| Workload | Mode | Median | Persistence |
|
|
394
|
+
| --- | --- | ---: | --- |
|
|
395
|
+
| Internal batched push, 1M jobs | Embedded | 729,395 jobs/sec | In-memory, no `dataPath` |
|
|
396
|
+
| Public sustained `addBulk`, 50K cell | Embedded | 186,384 jobs/sec | On-disk buffered SQLite |
|
|
397
|
+
| `PUSHB`, fresh 50K sample | TCP | 158,779 jobs/sec | On-disk buffered SQLite |
|
|
398
|
+
| No-work worker drain, concurrency 50 | TCP | 17,256 jobs/sec | Full pull/process/ACK |
|
|
399
|
+
| Linear Workflow Engine | Embedded / TCP | 2,700 / 3,187 workflows/sec | Workflow SQLite + 3 queue nodes |
|
|
400
|
+
|
|
401
|
+
These operations do different work; the internal in-memory result is not an
|
|
402
|
+
SQLite or public-API claim. Run `bun run bench`, `bun run bench:tcp`, or
|
|
403
|
+
`bun run bench:workflow` on your hardware.
|
|
404
|
+
[Benchmark methodology →](https://bunqueue.dev/guide/benchmarks/) ·
|
|
405
|
+
[full engineering report](docs/benchmarks/native-engineering-2026-07-30.md)
|
|
353
406
|
|
|
354
407
|
## Documentation
|
|
355
408
|
|
|
@@ -10,10 +10,11 @@ import * as dlqOps from './dlqManager';
|
|
|
10
10
|
import { checkExpiredLocks } from './lockManager';
|
|
11
11
|
import { cleanup } from './cleanupTasks';
|
|
12
12
|
import { checkStalledJobs } from './stallDetection';
|
|
13
|
-
import { processPendingDependencies } from './dependencyProcessor';
|
|
13
|
+
import { checkpointDependencyPromotion, dependencyReadyState, processPendingDependencies, } from './dependencyProcessor';
|
|
14
14
|
import { handleTaskError, handleTaskSuccess, getTaskErrorStats } from './taskErrorTracking';
|
|
15
15
|
import { runMonitoringChecks } from './monitoringChecks';
|
|
16
|
-
import { isCorruptDependsOn } from '../infrastructure/persistence/sqliteSerializer';
|
|
16
|
+
import { isCorruptDependsOn, persistedJobState, } from '../infrastructure/persistence/sqliteSerializer';
|
|
17
|
+
import { reconcileDependencyCompletionPins } from './dependencyCompletions';
|
|
17
18
|
export { getTaskErrorStats };
|
|
18
19
|
/**
|
|
19
20
|
* Start all background tasks
|
|
@@ -195,8 +196,14 @@ function quarantineCorruptDependsOn(ctx, job) {
|
|
|
195
196
|
export function recover(ctx) {
|
|
196
197
|
if (!ctx.storage)
|
|
197
198
|
return;
|
|
199
|
+
// Keep the full pre-reconciliation window outside the bounded RAM tracker.
|
|
200
|
+
// A restart may lower maxCompletedJobs; pruning before Phase 2 reconstructs
|
|
201
|
+
// reverse edges could discard an old proof still owned by a waiting parent.
|
|
202
|
+
const dependencyCompletions = ctx.storage.loadDependencyCompletions();
|
|
198
203
|
// Load completed job IDs from SQLite for dependency checking
|
|
199
204
|
const completedInDb = ctx.storage.loadCompletedJobIds();
|
|
205
|
+
for (const record of dependencyCompletions)
|
|
206
|
+
completedInDb.add(record.jobId);
|
|
200
207
|
// Load DLQ job IDs so Phase 1 can skip stale active rows for DLQ'd jobs
|
|
201
208
|
// (legacy DBs predate the DLQ-row cleanup fix in failJob).
|
|
202
209
|
const dlqJobIds = ctx.storage.loadDlqJobIds();
|
|
@@ -309,9 +316,15 @@ export function recover(ctx) {
|
|
|
309
316
|
continue;
|
|
310
317
|
}
|
|
311
318
|
// Check if job has unmet dependencies
|
|
312
|
-
//
|
|
319
|
+
// A ready persisted state is an authoritative checkpoint: dependency
|
|
320
|
+
// proofs are deliberately bounded and may have expired after promotion.
|
|
313
321
|
const hasDependencies = job.dependsOn && job.dependsOn.length > 0;
|
|
322
|
+
const recoveredState = persistedJobState(job);
|
|
323
|
+
const wasAlreadyPromoted = recoveredState === 'waiting' ||
|
|
324
|
+
recoveredState === 'prioritized' ||
|
|
325
|
+
recoveredState === 'delayed';
|
|
314
326
|
const needsWaitingDeps = hasDependencies &&
|
|
327
|
+
!wasAlreadyPromoted &&
|
|
315
328
|
!job.dependsOn.every((depId) => ctx.completedJobs.has(depId) || completedInDb.has(depId));
|
|
316
329
|
if (needsWaitingDeps) {
|
|
317
330
|
// Job is waiting for dependencies - don't add to main queue
|
|
@@ -323,8 +336,11 @@ export function recover(ctx) {
|
|
|
323
336
|
// Job is ready to process
|
|
324
337
|
shard.getQueue(job.queue).push(job);
|
|
325
338
|
// Update running counters for O(1) stats and temporal index
|
|
326
|
-
const
|
|
339
|
+
const state = dependencyReadyState(job, now);
|
|
340
|
+
const isDelayed = state === 'delayed';
|
|
327
341
|
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
342
|
+
if (hasDependencies)
|
|
343
|
+
checkpointDependencyPromotion(job, state, now, ctx.storage);
|
|
328
344
|
}
|
|
329
345
|
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
330
346
|
ctx.dependencyResults.registerConsumer(job.id, job.dependsOn);
|
|
@@ -416,6 +432,7 @@ export function recover(ctx) {
|
|
|
416
432
|
if (completedBatch.length < batchSize)
|
|
417
433
|
break;
|
|
418
434
|
}
|
|
435
|
+
reconcileDependencyCompletionPins(ctx);
|
|
419
436
|
}
|
|
420
437
|
// Re-export for backward compatibility
|
|
421
438
|
export { processPendingDependencies };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { processingShardIndex, SHARD_COUNT } from '../shared/hash';
|
|
6
6
|
import { withWriteLock } from '../shared/lock';
|
|
7
|
+
import { releaseDependencyCompletionPins } from './dependencyCompletions';
|
|
7
8
|
/**
|
|
8
9
|
* Main cleanup function - called periodically to maintain system health
|
|
9
10
|
* Cleans orphaned entries, stale data, and manages memory
|
|
@@ -72,6 +73,7 @@ async function cleanStaleWaitingDependencies(ctx, now) {
|
|
|
72
73
|
continue;
|
|
73
74
|
const removed = await withWriteLock(ctx.shardLocks[i], () => {
|
|
74
75
|
let count = 0;
|
|
76
|
+
const released = [];
|
|
75
77
|
for (const id of staleIds) {
|
|
76
78
|
const job = shard.waitingDeps.get(id);
|
|
77
79
|
if (!job || now - job.createdAt <= depTimeout)
|
|
@@ -81,6 +83,7 @@ async function cleanStaleWaitingDependencies(ctx, now) {
|
|
|
81
83
|
ctx.storage?.deleteJob(job.id);
|
|
82
84
|
shard.waitingDeps.delete(job.id);
|
|
83
85
|
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
86
|
+
released.push(...job.dependsOn);
|
|
84
87
|
if (job.uniqueKey && shard.getUniqueKeyEntry(job.queue, job.uniqueKey)?.jobId === job.id) {
|
|
85
88
|
shard.releaseUniqueKey(job.queue, job.uniqueKey);
|
|
86
89
|
}
|
|
@@ -91,10 +94,11 @@ async function cleanStaleWaitingDependencies(ctx, now) {
|
|
|
91
94
|
ctx.jobIndex.delete(job.id);
|
|
92
95
|
count++;
|
|
93
96
|
}
|
|
94
|
-
return count;
|
|
97
|
+
return { count, released };
|
|
95
98
|
});
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
releaseDependencyCompletionPins(removed.released, ctx);
|
|
100
|
+
if (removed.count > 0) {
|
|
101
|
+
ctx.dashboardEmit?.('cleanup:stale-deps-removed', { count: removed.count });
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
}
|
|
@@ -13,6 +13,7 @@ import type { WorkerManager } from './workerManager';
|
|
|
13
13
|
import type { EventsManager } from './eventsManager';
|
|
14
14
|
import type { MonitoringState } from './monitoringChecks';
|
|
15
15
|
import type { DependencyResultTracker } from './dependencyResultTracker';
|
|
16
|
+
import type { DependencyCompletionTracker } from './dependencyCompletions';
|
|
16
17
|
import type { LockContext, BackgroundContext, StatsContext } from './types';
|
|
17
18
|
import type { PushContext } from './operations/push';
|
|
18
19
|
import type { PullContext } from './operations/pull';
|
|
@@ -29,12 +30,13 @@ export interface ContextDependencies {
|
|
|
29
30
|
storage: SqliteStorage | null;
|
|
30
31
|
shards: Shard[];
|
|
31
32
|
shardLocks: RWLock[];
|
|
33
|
+
customIdLock: RWLock;
|
|
32
34
|
processingShards: Map<JobId, Job>[];
|
|
33
35
|
processingLocks: RWLock[];
|
|
34
36
|
jobIndex: Map<JobId, JobLocation>;
|
|
35
37
|
completedJobs: BoundedSet<JobId>;
|
|
36
38
|
completedJobsData: BoundedMap<JobId, Job>;
|
|
37
|
-
depCompletions?:
|
|
39
|
+
depCompletions?: DependencyCompletionTracker;
|
|
38
40
|
timedOutJobs?: BoundedSet<JobId>;
|
|
39
41
|
jobResults: LRUMap<JobId, unknown>;
|
|
40
42
|
dependencyResults: DependencyResultTracker;
|
|
@@ -77,12 +79,13 @@ export interface ContextCallbacks {
|
|
|
77
79
|
registerQueueName: (queue: string) => void;
|
|
78
80
|
unregisterQueueName: (queue: string) => void;
|
|
79
81
|
onJobCompleted: (completedId: JobId) => void;
|
|
82
|
+
onJobFailed?: (failedId: JobId) => void;
|
|
80
83
|
onJobsCompleted: (completedIds: JobId[]) => void;
|
|
81
84
|
hasPendingDeps: () => boolean;
|
|
82
85
|
onRepeat: (job: Job) => void;
|
|
83
86
|
emitDashboardEvent?: (event: string, data: Record<string, unknown>) => void;
|
|
84
|
-
onChildTerminalFailure?: (childJob: Job, error: string | undefined) => void
|
|
85
|
-
onChildDependencyOption?: (childJob: Job, error: string | undefined) => void
|
|
87
|
+
onChildTerminalFailure?: (childJob: Job, error: string | undefined) => Promise<void>;
|
|
88
|
+
onChildDependencyOption?: (childJob: Job, error: string | undefined) => Promise<void>;
|
|
86
89
|
}
|
|
87
90
|
/**
|
|
88
91
|
* Factory for building context objects
|
|
@@ -35,6 +35,7 @@ export class ContextFactory {
|
|
|
35
35
|
jobIndex: this.deps.jobIndex,
|
|
36
36
|
completedJobs: this.deps.completedJobs,
|
|
37
37
|
depCompletions: this.deps.depCompletions,
|
|
38
|
+
maxDependencyCompletions: this.deps.config.maxCompletedJobs,
|
|
38
39
|
timedOutJobs: this.deps.timedOutJobs,
|
|
39
40
|
jobResults: this.deps.jobResults,
|
|
40
41
|
dependencyResults: this.deps.dependencyResults,
|
|
@@ -82,9 +83,11 @@ export class ContextFactory {
|
|
|
82
83
|
storage: this.deps.storage,
|
|
83
84
|
shards: this.deps.shards,
|
|
84
85
|
shardLocks: this.deps.shardLocks,
|
|
86
|
+
customIdLock: this.deps.customIdLock,
|
|
85
87
|
completedJobs: this.deps.completedJobs,
|
|
86
88
|
completedJobsData: this.deps.completedJobsData,
|
|
87
89
|
depCompletions: this.deps.depCompletions,
|
|
90
|
+
maxDependencyCompletions: this.deps.config.maxCompletedJobs,
|
|
88
91
|
timedOutJobs: this.deps.timedOutJobs,
|
|
89
92
|
jobResults: this.deps.jobResults,
|
|
90
93
|
dependencyResults: this.deps.dependencyResults,
|
|
@@ -93,6 +96,7 @@ export class ContextFactory {
|
|
|
93
96
|
totalPushed: this.deps.metrics.totalPushed,
|
|
94
97
|
broadcast: this.deps.eventsManager.broadcast.bind(this.deps.eventsManager),
|
|
95
98
|
dashboardEmit: this.callbacks.emitDashboardEvent,
|
|
99
|
+
registerQueueName: this.callbacks.registerQueueName,
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
getPullContext() {
|
|
@@ -118,6 +122,7 @@ export class ContextFactory {
|
|
|
118
122
|
completedJobs: this.deps.completedJobs,
|
|
119
123
|
completedJobsData: this.deps.completedJobsData,
|
|
120
124
|
depCompletions: this.deps.depCompletions,
|
|
125
|
+
maxDependencyCompletions: this.deps.config.maxCompletedJobs,
|
|
121
126
|
jobResults: this.deps.jobResults,
|
|
122
127
|
dependencyResults: this.deps.dependencyResults,
|
|
123
128
|
jobIndex: this.deps.jobIndex,
|
|
@@ -127,6 +132,7 @@ export class ContextFactory {
|
|
|
127
132
|
perQueueMetrics: this.deps.perQueueMetrics,
|
|
128
133
|
broadcast: this.deps.eventsManager.broadcast.bind(this.deps.eventsManager),
|
|
129
134
|
onJobCompleted: this.callbacks.onJobCompleted,
|
|
135
|
+
onJobFailed: this.callbacks.onJobFailed,
|
|
130
136
|
onJobsCompleted: this.callbacks.onJobsCompleted,
|
|
131
137
|
needsBroadcast: this.deps.eventsManager.needsBroadcast.bind(this.deps.eventsManager),
|
|
132
138
|
hasPendingDeps: this.callbacks.hasPendingDeps,
|
|
@@ -147,6 +153,8 @@ export class ContextFactory {
|
|
|
147
153
|
jobLocks: this.deps.jobLocks,
|
|
148
154
|
clientJobs: this.deps.clientJobs,
|
|
149
155
|
dependencyResults: this.deps.dependencyResults,
|
|
156
|
+
depCompletions: this.deps.depCompletions,
|
|
157
|
+
maxDependencyCompletions: this.deps.config.maxCompletedJobs,
|
|
150
158
|
webhookManager: this.deps.webhookManager,
|
|
151
159
|
eventsManager: this.deps.eventsManager,
|
|
152
160
|
repeatChain: this.deps.repeatChain,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Job, JobId } from '../domain/types/job';
|
|
2
|
+
import type { Shard } from '../domain/queue/shard';
|
|
3
|
+
import type { SqliteStorage } from '../infrastructure/persistence/sqlite';
|
|
4
|
+
import type { SetLike } from '../shared/lru';
|
|
5
|
+
interface CompletionRecord {
|
|
6
|
+
jobId: JobId;
|
|
7
|
+
pinned: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Payload-free completion evidence has two retention classes:
|
|
11
|
+
* recent IDs are FIFO-bounded, while IDs referenced by live dependency edges
|
|
12
|
+
* stay pinned until their final consumer is durably resolved or removed.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DependencyCompletionTracker implements SetLike<JobId> {
|
|
15
|
+
private readonly onRecentEvict?;
|
|
16
|
+
private readonly recent;
|
|
17
|
+
private readonly pinned;
|
|
18
|
+
private readonly maxRecent;
|
|
19
|
+
constructor(maxRecent: number, onRecentEvict?: ((jobId: JobId) => void) | undefined);
|
|
20
|
+
add(jobId: JobId): void;
|
|
21
|
+
pin(jobId: JobId): void;
|
|
22
|
+
unpin(jobId: JobId, retainAsRecent: boolean): void;
|
|
23
|
+
hydrate(records: Iterable<CompletionRecord>): void;
|
|
24
|
+
pinnedValues(): IterableIterator<JobId>;
|
|
25
|
+
has(jobId: JobId): boolean;
|
|
26
|
+
delete(jobId: JobId): boolean;
|
|
27
|
+
clear(): void;
|
|
28
|
+
get size(): number;
|
|
29
|
+
}
|
|
30
|
+
export interface DependencyCompletionContext {
|
|
31
|
+
storage: SqliteStorage | null;
|
|
32
|
+
shards: Shard[];
|
|
33
|
+
depCompletions?: DependencyCompletionTracker;
|
|
34
|
+
maxDependencyCompletions: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function hasDependencyWaiters(shards: Shard[], jobId: JobId): boolean;
|
|
37
|
+
/** Persist a removeOnComplete transition before publishing its RAM evidence. */
|
|
38
|
+
export declare function commitRemovedCompletion(job: Pick<Job, 'id' | 'queue'>, ctx: DependencyCompletionContext, completedAt?: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Protect recent proofs when a newly accepted parent waits on another
|
|
41
|
+
* dependency. SQLite is updated first so RAM never advertises a stronger
|
|
42
|
+
* durability guarantee than the database.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pinReferencedCompletions(dependencyIds: Iterable<JobId>, ctx: Pick<DependencyCompletionContext, 'storage' | 'depCompletions'>): void;
|
|
45
|
+
/**
|
|
46
|
+
* Unpin candidates only after every shard has released its reverse edge.
|
|
47
|
+
* Storage returns the complete retained set because pruning can also evict
|
|
48
|
+
* older recent entries; hydrating from it keeps both RAM tiers exact.
|
|
49
|
+
*/
|
|
50
|
+
export declare function releaseDependencyCompletionPins(dependencyIds: Iterable<JobId>, ctx: DependencyCompletionContext): void;
|
|
51
|
+
/** Rebuild pin ownership from the authoritative reverse dependency indexes. */
|
|
52
|
+
export declare function reconcileDependencyCompletionPins(ctx: DependencyCompletionContext): void;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload-free completion evidence has two retention classes:
|
|
3
|
+
* recent IDs are FIFO-bounded, while IDs referenced by live dependency edges
|
|
4
|
+
* stay pinned until their final consumer is durably resolved or removed.
|
|
5
|
+
*/
|
|
6
|
+
export class DependencyCompletionTracker {
|
|
7
|
+
onRecentEvict;
|
|
8
|
+
recent = new Set();
|
|
9
|
+
pinned = new Set();
|
|
10
|
+
maxRecent;
|
|
11
|
+
constructor(maxRecent, onRecentEvict) {
|
|
12
|
+
this.onRecentEvict = onRecentEvict;
|
|
13
|
+
this.maxRecent = Math.max(1, Math.trunc(maxRecent));
|
|
14
|
+
}
|
|
15
|
+
add(jobId) {
|
|
16
|
+
if (this.pinned.has(jobId) || this.recent.has(jobId))
|
|
17
|
+
return;
|
|
18
|
+
if (this.recent.size >= this.maxRecent) {
|
|
19
|
+
const oldest = this.recent.values().next().value;
|
|
20
|
+
if (oldest !== undefined) {
|
|
21
|
+
this.recent.delete(oldest);
|
|
22
|
+
this.onRecentEvict?.(oldest);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
this.recent.add(jobId);
|
|
26
|
+
}
|
|
27
|
+
pin(jobId) {
|
|
28
|
+
this.recent.delete(jobId);
|
|
29
|
+
this.pinned.add(jobId);
|
|
30
|
+
}
|
|
31
|
+
unpin(jobId, retainAsRecent) {
|
|
32
|
+
this.pinned.delete(jobId);
|
|
33
|
+
if (retainAsRecent)
|
|
34
|
+
this.recent.add(jobId);
|
|
35
|
+
else
|
|
36
|
+
this.recent.delete(jobId);
|
|
37
|
+
}
|
|
38
|
+
hydrate(records) {
|
|
39
|
+
this.clear();
|
|
40
|
+
for (const record of records) {
|
|
41
|
+
if (record.pinned)
|
|
42
|
+
this.pin(record.jobId);
|
|
43
|
+
else
|
|
44
|
+
this.add(record.jobId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
pinnedValues() {
|
|
48
|
+
return this.pinned.values();
|
|
49
|
+
}
|
|
50
|
+
has(jobId) {
|
|
51
|
+
return this.pinned.has(jobId) || this.recent.has(jobId);
|
|
52
|
+
}
|
|
53
|
+
delete(jobId) {
|
|
54
|
+
const wasPinned = this.pinned.delete(jobId);
|
|
55
|
+
return this.recent.delete(jobId) || wasPinned;
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
this.recent.clear();
|
|
59
|
+
this.pinned.clear();
|
|
60
|
+
}
|
|
61
|
+
get size() {
|
|
62
|
+
return this.recent.size + this.pinned.size;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function hasDependencyWaiters(shards, jobId) {
|
|
66
|
+
return shards.some((shard) => (shard.getJobsWaitingFor(jobId)?.size ?? 0) > 0);
|
|
67
|
+
}
|
|
68
|
+
/** Persist a removeOnComplete transition before publishing its RAM evidence. */
|
|
69
|
+
export function commitRemovedCompletion(job, ctx, completedAt = Date.now()) {
|
|
70
|
+
const pinned = hasDependencyWaiters(ctx.shards, job.id);
|
|
71
|
+
ctx.storage?.commitRemovedCompletion(job, ctx.maxDependencyCompletions, pinned, completedAt);
|
|
72
|
+
if (pinned)
|
|
73
|
+
ctx.depCompletions?.pin(job.id);
|
|
74
|
+
else
|
|
75
|
+
ctx.depCompletions?.add(job.id);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Protect recent proofs when a newly accepted parent waits on another
|
|
79
|
+
* dependency. SQLite is updated first so RAM never advertises a stronger
|
|
80
|
+
* durability guarantee than the database.
|
|
81
|
+
*/
|
|
82
|
+
export function pinReferencedCompletions(dependencyIds, ctx) {
|
|
83
|
+
const ids = [...new Set(dependencyIds)].filter((id) => ctx.depCompletions?.has(id) ?? false);
|
|
84
|
+
if (ids.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
ctx.storage?.pinDependencyCompletions(ids);
|
|
87
|
+
for (const id of ids)
|
|
88
|
+
ctx.depCompletions?.pin(id);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Unpin candidates only after every shard has released its reverse edge.
|
|
92
|
+
* Storage returns the complete retained set because pruning can also evict
|
|
93
|
+
* older recent entries; hydrating from it keeps both RAM tiers exact.
|
|
94
|
+
*/
|
|
95
|
+
export function releaseDependencyCompletionPins(dependencyIds, ctx) {
|
|
96
|
+
const candidates = [...new Set(dependencyIds)].filter((id) => !hasDependencyWaiters(ctx.shards, id));
|
|
97
|
+
if (candidates.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
if (ctx.storage) {
|
|
100
|
+
const records = ctx.storage.unpinDependencyCompletions(candidates, ctx.maxDependencyCompletions);
|
|
101
|
+
ctx.depCompletions?.hydrate(records);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const id of candidates)
|
|
105
|
+
ctx.depCompletions?.unpin(id, true);
|
|
106
|
+
}
|
|
107
|
+
/** Rebuild pin ownership from the authoritative reverse dependency indexes. */
|
|
108
|
+
export function reconcileDependencyCompletionPins(ctx) {
|
|
109
|
+
const referenced = new Set();
|
|
110
|
+
for (const shard of ctx.shards) {
|
|
111
|
+
for (const dependencyId of shard.dependencyIndex.keys())
|
|
112
|
+
referenced.add(dependencyId);
|
|
113
|
+
}
|
|
114
|
+
if (ctx.storage) {
|
|
115
|
+
const records = ctx.storage.reconcileDependencyCompletionPins(referenced, ctx.maxDependencyCompletions);
|
|
116
|
+
ctx.depCompletions?.hydrate(records);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const id of ctx.depCompletions?.pinnedValues() ?? []) {
|
|
120
|
+
if (!referenced.has(id))
|
|
121
|
+
ctx.depCompletions?.unpin(id, true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
* Dependency Processor - Job dependency resolution
|
|
3
3
|
* Uses reverse index for O(m) where m = jobs waiting on completed deps
|
|
4
4
|
*/
|
|
5
|
+
import type { Job } from '../domain/types/job';
|
|
5
6
|
import type { BackgroundContext } from './types';
|
|
7
|
+
type DependencyReadyState = 'delayed' | 'prioritized' | 'waiting';
|
|
8
|
+
export declare function dependencyReadyState(job: Job, now: number): DependencyReadyState;
|
|
9
|
+
export declare function checkpointDependencyPromotion(job: Job, state: DependencyReadyState, now: number, storage: BackgroundContext['storage']): void;
|
|
6
10
|
/**
|
|
7
11
|
* Process pending dependency checks
|
|
8
12
|
* Resolves jobs whose dependencies have been completed
|
|
9
13
|
*/
|
|
10
14
|
export declare function processPendingDependencies(ctx: BackgroundContext): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
import { MAX_TIMELINE_ENTRIES } from '../domain/types/job';
|
|
6
6
|
import { SHARD_COUNT } from '../shared/hash';
|
|
7
7
|
import { withWriteLock } from '../shared/lock';
|
|
8
|
+
import { releaseDependencyCompletionPins } from './dependencyCompletions';
|
|
9
|
+
export function dependencyReadyState(job, now) {
|
|
10
|
+
if (job.runAt > now)
|
|
11
|
+
return 'delayed';
|
|
12
|
+
return job.priority > 0 ? 'prioritized' : 'waiting';
|
|
13
|
+
}
|
|
14
|
+
export function checkpointDependencyPromotion(job, state, now, storage) {
|
|
15
|
+
if (job.timeline.at(-1)?.state !== state && job.timeline.length < MAX_TIMELINE_ENTRIES) {
|
|
16
|
+
job.timeline.push({ state, timestamp: now });
|
|
17
|
+
}
|
|
18
|
+
storage?.updateFlowParentResolution(job, state);
|
|
19
|
+
}
|
|
8
20
|
/**
|
|
9
21
|
* Process pending dependency checks
|
|
10
22
|
* Resolves jobs whose dependencies have been completed
|
|
@@ -13,8 +25,10 @@ export async function processPendingDependencies(ctx) {
|
|
|
13
25
|
if (ctx.pendingDepChecks.size === 0)
|
|
14
26
|
return;
|
|
15
27
|
const completedIds = Array.from(ctx.pendingDepChecks);
|
|
28
|
+
const completedNow = new Set(completedIds);
|
|
16
29
|
ctx.pendingDepChecks.clear();
|
|
17
30
|
const jobsToCheckByShard = new Map();
|
|
31
|
+
const releasedDependencyIds = new Set();
|
|
18
32
|
// Find all jobs waiting for the completed dependencies
|
|
19
33
|
for (const completedId of completedIds) {
|
|
20
34
|
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
@@ -42,37 +56,35 @@ export async function processPendingDependencies(ctx) {
|
|
|
42
56
|
// dropped to bound memory), so also honor its bare-id depCompletions entry.
|
|
43
57
|
for (const jobId of jobIdsToCheck) {
|
|
44
58
|
const job = shard.waitingDeps.get(jobId);
|
|
45
|
-
if (job?.dependsOn.every((dep) =>
|
|
59
|
+
if (job?.dependsOn.every((dep) => completedNow.has(dep) ||
|
|
60
|
+
ctx.completedJobs.has(dep) ||
|
|
61
|
+
(ctx.depCompletions?.has(dep) ?? false))) {
|
|
46
62
|
jobsToPromote.push(job);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
65
|
// Promote jobs with all dependencies satisfied
|
|
50
66
|
if (jobsToPromote.length > 0) {
|
|
51
|
-
promoteJobsToQueue(jobsToPromote, shard, ctx, i);
|
|
67
|
+
promoteJobsToQueue(jobsToPromote, shard, ctx, i, releasedDependencyIds);
|
|
52
68
|
}
|
|
53
69
|
});
|
|
54
70
|
}));
|
|
55
|
-
|
|
56
|
-
// BoundedSet (same cap as completedJobs), so it self-bounds. Pruning eagerly
|
|
57
|
-
// once "no waiters remain" would orphan a dependent pushed AFTER a
|
|
58
|
-
// removeOnComplete parent completed — exactly the symmetry completedJobs
|
|
59
|
-
// provides for normal parents (readiness holds for the whole bounded window).
|
|
71
|
+
releaseDependencyCompletionPins(releasedDependencyIds, ctx);
|
|
60
72
|
}
|
|
61
73
|
/** Move jobs from waitingDeps to the active queue */
|
|
62
|
-
function promoteJobsToQueue(jobsToPromote, shard, ctx, shardIdx) {
|
|
74
|
+
function promoteJobsToQueue(jobsToPromote, shard, ctx, shardIdx, releasedDependencyIds) {
|
|
63
75
|
const now = Date.now();
|
|
64
76
|
for (const job of jobsToPromote) {
|
|
65
77
|
if (shard.waitingDeps.has(job.id)) {
|
|
78
|
+
const state = dependencyReadyState(job, now);
|
|
79
|
+
checkpointDependencyPromotion(job, state, now, ctx.storage);
|
|
66
80
|
shard.waitingDeps.delete(job.id);
|
|
67
81
|
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
82
|
+
for (const dependencyId of job.dependsOn)
|
|
83
|
+
releasedDependencyIds.add(dependencyId);
|
|
68
84
|
shard.getQueue(job.queue).push(job);
|
|
69
|
-
const isDelayed =
|
|
85
|
+
const isDelayed = state === 'delayed';
|
|
70
86
|
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
71
87
|
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx, queueName: job.queue });
|
|
72
|
-
if (job.timeline.length < MAX_TIMELINE_ENTRIES) {
|
|
73
|
-
const state = isDelayed ? 'delayed' : job.priority > 0 ? 'prioritized' : 'waiting';
|
|
74
|
-
job.timeline.push({ state, timestamp: now });
|
|
75
|
-
}
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
if (jobsToPromote.length > 0) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JobId } from '../domain/types/job';
|
|
2
|
+
import type { JobLocation } from '../domain/types/queue';
|
|
3
|
+
import type { Shard } from '../domain/queue/shard';
|
|
4
|
+
import type { SqliteStorage } from '../infrastructure/persistence/sqlite';
|
|
5
|
+
import type { SetLike } from '../shared/lru';
|
|
6
|
+
import type { DependencyResultTracker } from './dependencyResultTracker';
|
|
7
|
+
import { type DependencyCompletionTracker } from './dependencyCompletions';
|
|
8
|
+
export interface FlowFailureRecoveryContext {
|
|
9
|
+
readonly storage: SqliteStorage;
|
|
10
|
+
readonly shards: Shard[];
|
|
11
|
+
readonly jobIndex: Map<JobId, JobLocation>;
|
|
12
|
+
readonly completedJobs: SetLike<JobId>;
|
|
13
|
+
readonly depCompletions?: DependencyCompletionTracker;
|
|
14
|
+
readonly maxDependencyCompletions: number;
|
|
15
|
+
readonly dependencyResults: DependencyResultTracker;
|
|
16
|
+
readonly failedChildrenValues: Map<JobId, Record<string, string>>;
|
|
17
|
+
readonly ignoredChildrenFailures: Map<JobId, Record<string, string>>;
|
|
18
|
+
}
|
|
19
|
+
/** Replay the durable flow-failure outbox before workers can observe recovered jobs. */
|
|
20
|
+
export declare function recoverFlowFailures(ctx: FlowFailureRecoveryContext): void;
|