bunqueue 2.8.58 → 2.8.60

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 (61) hide show
  1. package/README.md +5 -1
  2. package/dist/application/dlqManager.d.ts +1 -1
  3. package/dist/application/dlqManager.js +1 -1
  4. package/dist/application/lockManager.js +1 -1
  5. package/dist/application/queue-manager/ack.js +1 -1
  6. package/dist/application/queue-manager/delivery.js +1 -1
  7. package/dist/application/statsManager.d.ts +1 -1
  8. package/dist/application/statsManager.js +1 -1
  9. package/dist/client/events.js +2 -2
  10. package/dist/client/flow.js +0 -1
  11. package/dist/client/queue/operations/add/single.js +1 -1
  12. package/dist/client/sandboxed/runtime/state.js +2 -2
  13. package/dist/client/tcp/connection.d.ts +1 -1
  14. package/dist/client/tcp/connection.js +1 -1
  15. package/dist/client/tcp/runtime/state.js +2 -2
  16. package/dist/client/types/metrics.d.ts +1 -1
  17. package/dist/client/worker/runtime/control.js +0 -1
  18. package/dist/client/worker/runtime/lifecycle.js +0 -1
  19. package/dist/client/worker/runtime/state.js +3 -3
  20. package/dist/client/workflow/compensationChild.d.ts +2 -0
  21. package/dist/client/workflow/compensationChild.js +3 -1
  22. package/dist/client/workflow/compensationPass.d.ts +1 -0
  23. package/dist/client/workflow/compensationPass.js +18 -2
  24. package/dist/client/workflow/compensator.d.ts +2 -1
  25. package/dist/client/workflow/compensator.js +11 -3
  26. package/dist/client/workflow/engine.js +1 -1
  27. package/dist/client/workflow/executionFence.d.ts +15 -0
  28. package/dist/client/workflow/executionFence.js +29 -0
  29. package/dist/client/workflow/executor.d.ts +5 -11
  30. package/dist/client/workflow/executor.js +48 -47
  31. package/dist/client/workflow/executorLifecycle.d.ts +1 -0
  32. package/dist/client/workflow/executorLifecycle.js +41 -10
  33. package/dist/client/workflow/executorNodes.d.ts +1 -0
  34. package/dist/client/workflow/executorNodes.js +45 -16
  35. package/dist/client/workflow/executorQueue.d.ts +9 -0
  36. package/dist/client/workflow/executorQueue.js +15 -0
  37. package/dist/client/workflow/forEachRunner.d.ts +4 -0
  38. package/dist/client/workflow/forEachRunner.js +70 -0
  39. package/dist/client/workflow/loops.d.ts +4 -5
  40. package/dist/client/workflow/loops.js +27 -93
  41. package/dist/client/workflow/mapRunner.d.ts +1 -1
  42. package/dist/client/workflow/mapRunner.js +11 -2
  43. package/dist/client/workflow/recovery.d.ts +1 -0
  44. package/dist/client/workflow/recovery.js +24 -18
  45. package/dist/client/workflow/rollbackControl.d.ts +1 -0
  46. package/dist/client/workflow/rollbackControl.js +5 -2
  47. package/dist/client/workflow/runner.d.ts +3 -1
  48. package/dist/client/workflow/runner.js +26 -3
  49. package/dist/client/workflow/stepTypes.js +1 -1
  50. package/dist/client/workflow/subWorkflowRunner.d.ts +1 -0
  51. package/dist/client/workflow/subWorkflowRunner.js +7 -0
  52. package/dist/client/workflow/waitFor.d.ts +4 -0
  53. package/dist/client/workflow/waitFor.js +23 -11
  54. package/dist/client/workflow/workflowDecisions.d.ts +1 -1
  55. package/dist/client/workflow/workflowDecisions.js +3 -1
  56. package/dist/infrastructure/persistence/sqlite.d.ts +1 -1
  57. package/dist/infrastructure/server/handler-routes/jobs.js +0 -3
  58. package/dist/infrastructure/server/handlerRoutes.d.ts +2 -2
  59. package/dist/infrastructure/server/handlerRoutes.js +2 -2
  60. package/dist/infrastructure/server/httpEndpoints.js +2 -0
  61. package/package.json +12 -10
@@ -23,6 +23,7 @@ const TIMEOUT_ENQUEUE_RETRY_MS = 5_000;
23
23
  */
24
24
  export function scheduleTimeoutCheck(deps, execId, workflowName, nodeIdx, ms) {
25
25
  const arm = (requestedDelay) => {
26
+ deps.assertActive();
26
27
  const delay = Math.min(Math.max(requestedDelay, 0), MAX_TIMER_MS);
27
28
  // Replacing a live timer for the same execution — re-entering a waitFor node used
28
29
  // to leak the previous one, which then fired against a node the run had left.
@@ -30,6 +31,11 @@ export function scheduleTimeoutCheck(deps, execId, workflowName, nodeIdx, ms) {
30
31
  if (previous)
31
32
  clock().clearTimeout(previous);
32
33
  const timer = clock().setTimeout(() => {
34
+ if (!deps.isActive()) {
35
+ if (deps.timers.get(execId) === timer)
36
+ deps.timers.delete(execId);
37
+ return;
38
+ }
33
39
  const jobData = { executionId: execId, workflowName, nodeIndex: nodeIdx };
34
40
  const published = () => {
35
41
  // Keep the handle in the map while add() is pending. signal() and close() use
@@ -40,6 +46,10 @@ export function scheduleTimeoutCheck(deps, execId, workflowName, nodeIdx, ms) {
40
46
  const failed = () => {
41
47
  if (deps.timers.get(execId) !== timer)
42
48
  return;
49
+ if (!deps.isActive()) {
50
+ deps.timers.delete(execId);
51
+ return;
52
+ }
43
53
  arm(TIMEOUT_ENQUEUE_RETRY_MS);
44
54
  };
45
55
  try {
@@ -75,6 +85,7 @@ export function clearTimers(timers) {
75
85
  * short-circuits without treating the pause as an error.
76
86
  */
77
87
  export async function runWaitFor(deps, exec, node, idx, wf) {
88
+ deps.assertActive();
78
89
  if (hasSignal(exec.signals, node.event)) {
79
90
  await deps.advance(exec, idx + 1, wf);
80
91
  return;
@@ -92,11 +103,14 @@ export async function runWaitFor(deps, exec, node, idx, wf) {
92
103
  // (refund, release stock) work the approver just authorised.
93
104
  const fresh = deps.store.get(exec.id);
94
105
  if (fresh && hasSignal(fresh.signals, node.event)) {
106
+ deps.assertActive();
95
107
  exec.signals = fresh.signals;
96
108
  await deps.advance(exec, idx + 1, wf);
97
109
  return;
98
110
  }
111
+ deps.assertActive();
99
112
  deps.emitter?.emitSignal('signal:timeout', exec.id, exec.workflowName, node.event);
113
+ deps.assertActive();
100
114
  const timeoutReason = `Signal "${node.event}" timed out after ${node.timeout}ms`;
101
115
  exec.steps[waitKey] = {
102
116
  status: 'failed',
@@ -106,30 +120,26 @@ export async function runWaitFor(deps, exec, node, idx, wf) {
106
120
  };
107
121
  exec.state = 'failed';
108
122
  exec.failureReason = timeoutReason;
109
- // Unguarded on purpose, same reasoning as the failure write in `executor.ts`. Disk
110
- // still says `waiting`, which `listRecoverable()` covers, so `recoverWaiting`
111
- // recomputes the remaining time, re-enqueues, and the gate times out again rather
112
- // than leaving a run that looks settled and was never unwound.
113
- //
114
- // A throw here skips the compensate CALL below but not the compensation: it
115
- // propagates into `runNode`'s catch, which compensates anyway. That is the generic
116
- // path the `WaitForSignalError` sentinel below exists to avoid on the normal route,
117
- // so the outcome is a redundant route, not a missing rollback.
118
- deps.store.update(exec);
123
+ // Persist failure before starting rollback. The lifecycle guard leaves the
124
+ // durable row waiting if force-close won, so replacement recovery re-evaluates
125
+ // the same elapsed deadline without starting an old executor's compensation.
126
+ deps.updateFn(exec);
119
127
  // Compensate here, then signal completion via the WaitForSignalError sentinel
120
128
  // so processStep short-circuits (return null) instead of re-running
121
129
  // compensation through its generic catch path.
122
130
  await deps.compensate(exec, wf);
131
+ deps.assertActive();
123
132
  deps.emitter?.emitWorkflow('workflow:failed', exec.id, exec.workflowName, 'failed');
124
133
  throw new WaitForSignalError(node.event);
125
134
  }
126
- deps.store.update(exec);
135
+ deps.updateFn(exec);
127
136
  remaining = node.timeout - (clock().now() - waitingSince);
128
137
  }
129
138
  // Park transactionally. Between the in-memory check above and this point a signal
130
139
  // can land: it would be recorded with no parked run left to claim the resume,
131
140
  // hanging the execution forever. parkForSignal() re-reads the persisted signals
132
141
  // and only transitions 'running' -> 'waiting' when none has arrived.
142
+ deps.assertActive();
133
143
  const park = deps.store.parkForSignal(exec.id, node.event);
134
144
  if (park.signalPresent) {
135
145
  exec.signals = park.signals;
@@ -143,8 +153,10 @@ export async function runWaitFor(deps, exec, node, idx, wf) {
143
153
  }
144
154
  exec.state = 'waiting';
145
155
  if (node.timeout !== undefined) {
156
+ deps.assertActive();
146
157
  deps.scheduleTimeoutCheck(exec.id, exec.workflowName, idx, remaining);
147
158
  }
159
+ deps.assertActive();
148
160
  deps.emitter?.emitWorkflow('workflow:waiting', exec.id, exec.workflowName, 'waiting');
149
161
  throw new WaitForSignalError(node.event);
150
162
  }
@@ -4,7 +4,7 @@ import type { Execution } from './types';
4
4
  * Return an already persisted decision, or evaluate and persist it before the caller
5
5
  * performs any effects selected by that decision.
6
6
  */
7
- export declare function resolveDecision<T>(exec: Execution, key: string, evaluate: () => T | Promise<T>, updateFn: (exec: Execution) => void): Promise<T>;
7
+ export declare function resolveDecision<T>(exec: Execution, key: string, evaluate: () => T | Promise<T>, updateFn: (exec: Execution) => void, assertActive: () => void): Promise<T>;
8
8
  export declare function branchDecisionKey(nodeIndex: number): string;
9
9
  export declare function forEachItemsDecisionKey(stepName: string): string;
10
10
  export declare function loopDecisionKey(kind: 'doUntil' | 'doWhile', firstStepName: string, iteration: number): string;
@@ -4,11 +4,13 @@ const hasOwn = (value, key) => Object.hasOwn(value, key);
4
4
  * Return an already persisted decision, or evaluate and persist it before the caller
5
5
  * performs any effects selected by that decision.
6
6
  */
7
- export async function resolveDecision(exec, key, evaluate, updateFn) {
7
+ export async function resolveDecision(exec, key, evaluate, updateFn, assertActive) {
8
+ assertActive();
8
9
  const decisions = (exec.decisions ??= {});
9
10
  if (hasOwn(decisions, key))
10
11
  return decisions[key];
11
12
  const value = await evaluate();
13
+ assertActive();
12
14
  decisions[key] = value;
13
15
  updateFn(exec);
14
16
  return value;
@@ -1,5 +1,5 @@
1
1
  import { SqliteLifecycle } from './sqlite/lifecycle';
2
- export type { SqliteConfig, SqliteCriticalLoss, SqliteCriticalLossCallback, } from './types/sqlite';
2
+ export type { SqliteConfig, SqliteCriticalLoss, SqliteCriticalLossCallback } from './types/sqlite';
3
3
  export type { DurableAdmissionMetadata } from './types/admission';
4
4
  /** SQLite persistence façade. Implementation is split by responsibility. */
5
5
  export declare class SqliteStorage extends SqliteLifecycle {
@@ -5,7 +5,6 @@ import { handleGetDeduplicationJobId, handleGetQueueLimits, handleMoveToWaitingC
5
5
  import { handleCancel, handleGetProgress, handleProgress } from '../handlers/management';
6
6
  import { handleExtendLock, handleExtendLocks } from '../handlers/monitoring';
7
7
  import { handleGetChildrenValues, handleGetCountsPerPriority, handleGetJob, handleGetJobByCustomId, handleGetJobCounts, handleGetJobs, handleGetResult, handleGetState, } from '../handlers/query';
8
- // biome-ignore lint/suspicious/useAwait: stable router API always returns a Promise.
9
8
  export async function routeCoreCommand(command, context, requestId) {
10
9
  switch (command.cmd) {
11
10
  case 'PUSH':
@@ -28,7 +27,6 @@ export async function routeCoreCommand(command, context, requestId) {
28
27
  return null;
29
28
  }
30
29
  }
31
- // biome-ignore lint/suspicious/useAwait: stable router API always returns a Promise.
32
30
  export async function routeQueryCommand(command, context, requestId) {
33
31
  switch (command.cmd) {
34
32
  case 'GetJob':
@@ -59,7 +57,6 @@ export async function routeQueryCommand(command, context, requestId) {
59
57
  return null;
60
58
  }
61
59
  }
62
- // biome-ignore lint/suspicious/useAwait: stable router API always returns a Promise.
63
60
  export async function routeManagementCommand(command, context, requestId) {
64
61
  switch (command.cmd) {
65
62
  case 'Cancel':
@@ -1,3 +1,3 @@
1
1
  export { routeConfigCommand, routeCronCommand, routeDlqCommand, routeQueueControlCommand, routeRateLimitCommand, } from './handler-routes/control';
2
- export { routeCoreCommand, routeManagementCommand, routeQueryCommand, } from './handler-routes/jobs';
3
- export { routeDashboardCommand, routeMonitoringCommand, } from './handler-routes/monitoring';
2
+ export { routeCoreCommand, routeManagementCommand, routeQueryCommand } from './handler-routes/jobs';
3
+ export { routeDashboardCommand, routeMonitoringCommand } from './handler-routes/monitoring';
@@ -1,3 +1,3 @@
1
1
  export { routeConfigCommand, routeCronCommand, routeDlqCommand, routeQueueControlCommand, routeRateLimitCommand, } from './handler-routes/control';
2
- export { routeCoreCommand, routeManagementCommand, routeQueryCommand, } from './handler-routes/jobs';
3
- export { routeDashboardCommand, routeMonitoringCommand, } from './handler-routes/monitoring';
2
+ export { routeCoreCommand, routeManagementCommand, routeQueryCommand } from './handler-routes/jobs';
3
+ export { routeDashboardCommand, routeMonitoringCommand } from './handler-routes/monitoring';
@@ -84,6 +84,7 @@ export function readinessEndpoint(queueManager, corsOrigins) {
84
84
  /** GC endpoint - force garbage collection */
85
85
  export function gcEndpoint(queueManager) {
86
86
  const before = process.memoryUsage();
87
+ // oxlint-disable-next-line typescript/prefer-optional-chain -- the typeof guard avoids referencing an unavailable Bun global
87
88
  if (typeof Bun !== 'undefined' && Bun.gc) {
88
89
  Bun.gc(true);
89
90
  }
@@ -105,6 +106,7 @@ export function gcEndpoint(queueManager) {
105
106
  }
106
107
  /** Heap stats endpoint - for debugging memory leaks */
107
108
  export async function heapStatsEndpoint(queueManager) {
109
+ // oxlint-disable-next-line typescript/prefer-optional-chain -- the typeof guard avoids referencing an unavailable Bun global
108
110
  if (typeof Bun !== 'undefined' && Bun.gc) {
109
111
  Bun.gc(true);
110
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue",
3
- "version": "2.8.58",
3
+ "version": "2.8.60",
4
4
  "description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external infrastructure.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -71,13 +71,13 @@
71
71
  "bench:workflow:scale": "bun run bench/workflow-engine/scale.ts",
72
72
  "bench:joblist": "bun run bench/job-list-perf.ts",
73
73
  "bench:compare": "bun run bench/comparison/run.ts",
74
- "lint": "biome lint src",
75
- "lint:fix": "biome lint --write src",
76
- "format": "biome format --write src",
77
- "format:check": "biome format src",
74
+ "lint": "oxlint src",
75
+ "lint:fix": "oxlint --fix src",
76
+ "format": "oxfmt --write src",
77
+ "format:check": "oxfmt --check src",
78
78
  "typecheck": "tsc --noEmit",
79
- "check:biome": "biome check src",
80
- "check": "bun run typecheck && biome check src && bun run check:docs-data",
79
+ "check:oxc": "bun run lint && bun run format:check",
80
+ "check": "bun run typecheck && bun run check:oxc && bun run check:docs-data",
81
81
  "prepublishOnly": "bun run build:lib"
82
82
  },
83
83
  "dependencies": {
@@ -87,18 +87,20 @@
87
87
  "devDependencies": {
88
88
  "@ai-sdk/anthropic": "^4.0.22",
89
89
  "@anthropic-ai/claude-agent-sdk": "^0.3.220",
90
- "@biomejs/biome": "2.5.1",
91
90
  "@langchain/core": "^1.2.3",
92
91
  "@langchain/langgraph": "^1.4.8",
93
92
  "@mastra/core": "^1.53.0",
94
93
  "@modelcontextprotocol/sdk": "^1.26.0",
95
94
  "@openai/agents": "^0.13.5",
96
- "@types/bun": "^1.3.9",
95
+ "@types/bun": "^1.4.0",
97
96
  "ai": "^7.0.38",
98
97
  "bullmq": "^5.79.3",
99
98
  "elysia": "^1.4.25",
100
99
  "fast-check": "^4.9.0",
101
100
  "ioredis": "^5.11.1",
101
+ "oxfmt": "0.64.0",
102
+ "oxlint": "1.79.0",
103
+ "oxlint-tsgolint": "7.0.2001",
102
104
  "typedoc": "^0.28.20",
103
105
  "typescript": "^5.9.3",
104
106
  "zod": "^4.3.6"
@@ -162,6 +164,6 @@
162
164
  },
163
165
  "homepage": "https://bunqueue.dev",
164
166
  "engines": {
165
- "bun": ">=1.3.9"
167
+ "bun": ">=1.4.0"
166
168
  }
167
169
  }