bunqueue 2.8.30 → 2.8.31
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.
|
@@ -47,18 +47,45 @@ function tryDequeueNextJob(shard, queue, now, ctx) {
|
|
|
47
47
|
if (job.timeline.length < MAX_TIMELINE_ENTRIES) {
|
|
48
48
|
job.timeline.push({ state: 'active', timestamp: now });
|
|
49
49
|
}
|
|
50
|
+
// Make the queue -> processing transition atomic for observers. From the
|
|
51
|
+
// pop above, the job is no longer findable in the shard queue, so a
|
|
52
|
+
// jobIndex entry still saying 'queue' would make getJob/getJobState return
|
|
53
|
+
// a false null/'unknown' for an existing job (JOB -> null -> JOB(active)
|
|
54
|
+
// flicker), and cleanOrphanedJobIndex would even drop its index entry.
|
|
55
|
+
// Insert into processingShards and flip the index HERE, in the same
|
|
56
|
+
// synchronous critical section as the pop (the caller holds the shard
|
|
57
|
+
// write lock; JS is single-threaded, so no other task can interleave).
|
|
58
|
+
// Writing processingShards without its RWLock is safe: until this
|
|
59
|
+
// statement the index said 'queue', so no id-targeted critical section
|
|
60
|
+
// (ack/fail/discard operate only on ids they find as 'processing') can be
|
|
61
|
+
// mid-operation on this id; a sync Map.set cannot corrupt a paused
|
|
62
|
+
// critical section; and lock-free sync access to processingShards is
|
|
63
|
+
// established practice (stallDetection phases 1/2, collectActiveJobs,
|
|
64
|
+
// getJobProgress). Acquiring the async RWLock here would instead hold the
|
|
65
|
+
// hot shard write lock across an await.
|
|
66
|
+
const procIdx = processingShardIndex(job.id);
|
|
67
|
+
ctx.processingShards[procIdx].set(job.id, job);
|
|
68
|
+
ctx.jobIndex.set(job.id, { type: 'processing', shardIdx: procIdx });
|
|
50
69
|
return { status: 'job', job };
|
|
51
70
|
}
|
|
52
71
|
/**
|
|
53
|
-
*
|
|
72
|
+
* Finish the handoff of a dequeued job: persist active state and broadcast.
|
|
73
|
+
*
|
|
74
|
+
* The job was ALREADY inserted into processingShards and jobIndex was flipped
|
|
75
|
+
* to 'processing' inside the dequeue critical section (tryDequeueNextJob),
|
|
76
|
+
* atomically with the queue pop, so readers never see a stale 'queue'
|
|
77
|
+
* location. Only the bookkeeping that may follow an await happens here.
|
|
78
|
+
*
|
|
79
|
+
* Returns false when the job is no longer in processingShards: a management
|
|
80
|
+
* op (discardJob, moveJobToDelayed, obliterate) claimed it between the
|
|
81
|
+
* dequeue and this call. The claimer owns the job now; the pull must NOT
|
|
82
|
+
* deliver it to a worker nor mark it active.
|
|
54
83
|
*/
|
|
55
|
-
|
|
84
|
+
function finalizeProcessing(job, queue, ctx) {
|
|
56
85
|
const procIdx = processingShardIndex(job.id);
|
|
86
|
+
if (!ctx.processingShards[procIdx].has(job.id))
|
|
87
|
+
return false;
|
|
57
88
|
const now = Date.now();
|
|
58
|
-
await withWriteLock(ctx.processingLocks[procIdx], () => {
|
|
59
|
-
ctx.processingShards[procIdx].set(job.id, job);
|
|
60
|
-
});
|
|
61
|
-
ctx.jobIndex.set(job.id, { type: 'processing', shardIdx: procIdx });
|
|
62
89
|
try {
|
|
63
90
|
ctx.storage?.markActive(job.id, job.startedAt ?? now, job.timeline);
|
|
64
91
|
}
|
|
@@ -74,74 +101,59 @@ async function moveToProcessing(job, queue, ctx) {
|
|
|
74
101
|
jobId: job.id,
|
|
75
102
|
timestamp: now,
|
|
76
103
|
});
|
|
104
|
+
return true;
|
|
77
105
|
}
|
|
78
106
|
/**
|
|
79
|
-
*
|
|
107
|
+
* Finish the handoff for a batch of dequeued jobs.
|
|
108
|
+
* Returns the jobs actually delivered (claimed jobs are skipped, see above).
|
|
80
109
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const byProcShard = new Map();
|
|
110
|
+
function finalizeProcessingBatch(jobs, queue, ctx) {
|
|
111
|
+
const delivered = [];
|
|
84
112
|
for (const job of jobs) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (shardJobs.length === 0)
|
|
88
|
-
byProcShard.set(procIdx, shardJobs);
|
|
89
|
-
shardJobs.push(job);
|
|
90
|
-
}
|
|
91
|
-
// Add to processing shards in parallel
|
|
92
|
-
const lockPromises = [];
|
|
93
|
-
for (const [procIdx, shardJobs] of byProcShard) {
|
|
94
|
-
lockPromises.push(withWriteLock(ctx.processingLocks[procIdx], () => {
|
|
95
|
-
for (const job of shardJobs) {
|
|
96
|
-
ctx.processingShards[procIdx].set(job.id, job);
|
|
97
|
-
}
|
|
98
|
-
}));
|
|
99
|
-
}
|
|
100
|
-
await Promise.all(lockPromises);
|
|
101
|
-
// Update indexes and broadcast
|
|
102
|
-
const now = Date.now();
|
|
103
|
-
for (const job of jobs) {
|
|
104
|
-
const procIdx = processingShardIndex(job.id);
|
|
105
|
-
ctx.jobIndex.set(job.id, { type: 'processing', shardIdx: procIdx });
|
|
106
|
-
try {
|
|
107
|
-
ctx.storage?.markActive(job.id, job.startedAt ?? now, job.timeline);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Non-fatal: job is already in processingShards (in-memory source of truth).
|
|
111
|
-
}
|
|
112
|
-
ctx.totalPulled.value++;
|
|
113
|
-
throughputTracker.pullRate.increment();
|
|
114
|
-
ctx.broadcast({
|
|
115
|
-
eventType: 'pulled',
|
|
116
|
-
queue,
|
|
117
|
-
jobId: job.id,
|
|
118
|
-
timestamp: now,
|
|
119
|
-
});
|
|
113
|
+
if (finalizeProcessing(job, queue, ctx))
|
|
114
|
+
delivered.push(job);
|
|
120
115
|
}
|
|
116
|
+
return delivered;
|
|
121
117
|
}
|
|
122
118
|
/**
|
|
123
|
-
* Requeue a job
|
|
124
|
-
*
|
|
119
|
+
* Requeue a job whose handoff to the worker failed after dequeue.
|
|
120
|
+
* The dequeue critical section moved the job to processingShards and flipped
|
|
121
|
+
* jobIndex to 'processing'; this restores both, keeping the same atomicity
|
|
122
|
+
* rule: the job becomes findable in the queue (push + index flip in ONE
|
|
123
|
+
* shard-lock section) BEFORE the processing entry is dropped, so a reader
|
|
124
|
+
* never sees a location that misses. Lock order shard -> processing matches
|
|
125
|
+
* the documented hierarchy.
|
|
125
126
|
*/
|
|
126
127
|
async function requeueJob(job, queue, idx, ctx) {
|
|
127
|
-
// Remove from processingShards (may have been added before the failure)
|
|
128
128
|
const procIdx = processingShardIndex(job.id);
|
|
129
|
-
|
|
130
|
-
ctx.processingShards[procIdx].delete(job.id);
|
|
131
|
-
});
|
|
132
|
-
// Reset job state (was modified in tryDequeueNextJob)
|
|
133
|
-
job.startedAt = null;
|
|
134
|
-
// Push back to queue
|
|
129
|
+
let requeued = false;
|
|
135
130
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
131
|
+
// A management op (discardJob, moveJobToDelayed) may have claimed the job
|
|
132
|
+
// from processingShards after the failed handoff; it owns the job now.
|
|
133
|
+
if (!ctx.processingShards[procIdx].has(job.id))
|
|
134
|
+
return;
|
|
136
135
|
const shard = ctx.shards[idx];
|
|
137
136
|
if (job.groupId)
|
|
138
137
|
shard.releaseGroup(queue, job.groupId);
|
|
139
138
|
shard.releaseConcurrency(queue);
|
|
139
|
+
// Reset job state (was modified in tryDequeueNextJob)
|
|
140
|
+
job.startedAt = null;
|
|
140
141
|
shard.getQueue(queue).push(job);
|
|
141
142
|
shard.incrementQueued(job.id, false, job.createdAt, queue, job.runAt);
|
|
143
|
+
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: queue });
|
|
142
144
|
shard.notify();
|
|
145
|
+
requeued = true;
|
|
146
|
+
});
|
|
147
|
+
if (!requeued)
|
|
148
|
+
return;
|
|
149
|
+
await withWriteLock(ctx.processingLocks[procIdx], () => {
|
|
150
|
+
// A concurrent pull may have re-popped the job between the two lock
|
|
151
|
+
// sections and flipped the index back to 'processing'; do not delete the
|
|
152
|
+
// (re-inserted, same-object) entry out from under it.
|
|
153
|
+
if (ctx.jobIndex.get(job.id)?.type !== 'processing') {
|
|
154
|
+
ctx.processingShards[procIdx].delete(job.id);
|
|
155
|
+
}
|
|
143
156
|
});
|
|
144
|
-
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: queue });
|
|
145
157
|
}
|
|
146
158
|
/**
|
|
147
159
|
* Pull next job from queue
|
|
@@ -153,14 +165,18 @@ export async function pullJob(queue, timeoutMs, ctx) {
|
|
|
153
165
|
while (true) {
|
|
154
166
|
const job = await tryPullFromShard(queue, idx, ctx);
|
|
155
167
|
if (job) {
|
|
168
|
+
let delivered = false;
|
|
156
169
|
try {
|
|
157
|
-
|
|
170
|
+
delivered = finalizeProcessing(job, queue, ctx);
|
|
158
171
|
}
|
|
159
172
|
catch {
|
|
160
|
-
// Safety net: if
|
|
173
|
+
// Safety net: if the handoff fails, push job back to prevent loss
|
|
161
174
|
await requeueJob(job, queue, idx, ctx);
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
177
|
+
// Claimed by a management op during the handoff: try the next job
|
|
178
|
+
if (!delivered)
|
|
179
|
+
continue;
|
|
164
180
|
latencyTracker.pull.observe((Bun.nanoseconds() - startNs) / 1e6);
|
|
165
181
|
return job;
|
|
166
182
|
}
|
|
@@ -216,21 +232,27 @@ export async function pullJobBatch(queue, count, timeoutMs, ctx) {
|
|
|
216
232
|
while (true) {
|
|
217
233
|
const jobs = await tryPullBatchFromShard(queue, idx, count, ctx);
|
|
218
234
|
if (jobs.length > 0) {
|
|
235
|
+
let delivered;
|
|
219
236
|
try {
|
|
220
|
-
|
|
237
|
+
delivered = finalizeProcessingBatch(jobs, queue, ctx);
|
|
221
238
|
}
|
|
222
239
|
catch {
|
|
223
|
-
// Safety net: push all jobs back to prevent loss
|
|
240
|
+
// Safety net: push all jobs back to prevent loss (requeueJob skips
|
|
241
|
+
// jobs a management op already claimed)
|
|
224
242
|
for (const job of jobs) {
|
|
225
243
|
await requeueJob(job, queue, idx, ctx);
|
|
226
244
|
}
|
|
227
245
|
return [];
|
|
228
246
|
}
|
|
229
|
-
if (
|
|
230
|
-
|
|
247
|
+
if (delivered.length > 0) {
|
|
248
|
+
if (delivered.length > 1) {
|
|
249
|
+
ctx.dashboardEmit?.('batch:pulled', { queue, count: delivered.length });
|
|
250
|
+
}
|
|
251
|
+
latencyTracker.pull.observe((Bun.nanoseconds() - startNs) / 1e6);
|
|
252
|
+
return delivered;
|
|
231
253
|
}
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
// Every dequeued job was claimed by a management op: fall through to
|
|
255
|
+
// the timeout/wait logic below (nothing to deliver this round)
|
|
234
256
|
}
|
|
235
257
|
// No jobs available, check timeout
|
|
236
258
|
// Cache Date.now() to avoid multiple syscalls per iteration
|
|
@@ -6,51 +6,75 @@ import { withReadLock } from '../../shared/lock';
|
|
|
6
6
|
import { shardIndex } from '../../shared/hash';
|
|
7
7
|
/** Get job by ID */
|
|
8
8
|
export async function getJob(jobId, ctx) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return await withReadLock(ctx.shardLocks[location.shardIdx], () => {
|
|
26
|
-
const shard = ctx.shards[location.shardIdx];
|
|
27
|
-
return (shard.getQueue(location.queueName).find(jobId) ??
|
|
28
|
-
shard.waitingDeps.get(jobId) ??
|
|
29
|
-
shard.waitingChildren.get(jobId) ??
|
|
30
|
-
null);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
case 'processing': {
|
|
34
|
-
return await withReadLock(ctx.processingLocks[location.shardIdx], () => {
|
|
35
|
-
return ctx.processingShards[location.shardIdx].get(jobId) ?? null;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
case 'completed':
|
|
39
|
-
return ctx.storage?.getJob(jobId) ?? ctx.completedJobsData.get(jobId) ?? null;
|
|
40
|
-
case 'dlq': {
|
|
9
|
+
// The location snapshot is only valid until the first await: while this
|
|
10
|
+
// reader waits on the shard read lock (writers have priority), a concurrent
|
|
11
|
+
// pull can pop the job and move it queue -> processing. The pull updates
|
|
12
|
+
// jobIndex atomically with the pop (tryDequeueNextJob), so on a miss the
|
|
13
|
+
// index re-read below is authoritative: an identity change means the job
|
|
14
|
+
// MOVED, and we chase the new location instead of returning a false null
|
|
15
|
+
// for a job that still exists (getJob(id) === null must be permanent for
|
|
16
|
+
// never-reused uuidv7 ids). Bounded: every extra pass requires a further
|
|
17
|
+
// state transition of this very job (queue -> active -> completed/removed),
|
|
18
|
+
// so 4 passes cover any realistic chase; index entries are always replaced
|
|
19
|
+
// (never mutated), which makes the identity comparison exact.
|
|
20
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
21
|
+
const location = ctx.jobIndex.get(jobId);
|
|
22
|
+
if (!location) {
|
|
23
|
+
// Fallback: jobIndex may not be populated after restart for completed/DLQ jobs.
|
|
24
|
+
// Consult SQLite directly so getJob survives recovery.
|
|
41
25
|
if (ctx.storage) {
|
|
42
|
-
const dlqEntry = ctx.storage.getDlqEntry(jobId);
|
|
43
|
-
if (dlqEntry)
|
|
44
|
-
return dlqEntry.job;
|
|
45
26
|
const job = ctx.storage.getJob(jobId);
|
|
46
27
|
if (job)
|
|
47
28
|
return job;
|
|
29
|
+
const dlqEntry = ctx.storage.getDlqEntry(jobId);
|
|
30
|
+
if (dlqEntry)
|
|
31
|
+
return dlqEntry.job;
|
|
48
32
|
}
|
|
49
|
-
|
|
50
|
-
const dlqJobs = ctx.shards[dlqShardIdx].getDlq(location.queueName);
|
|
51
|
-
return dlqJobs.find((j) => j.id === jobId) ?? null;
|
|
33
|
+
return ctx.completedJobsData.get(jobId) ?? null;
|
|
52
34
|
}
|
|
35
|
+
let found = null;
|
|
36
|
+
switch (location.type) {
|
|
37
|
+
case 'queue': {
|
|
38
|
+
found = await withReadLock(ctx.shardLocks[location.shardIdx], () => {
|
|
39
|
+
const shard = ctx.shards[location.shardIdx];
|
|
40
|
+
return (shard.getQueue(location.queueName).find(jobId) ??
|
|
41
|
+
shard.waitingDeps.get(jobId) ??
|
|
42
|
+
shard.waitingChildren.get(jobId) ??
|
|
43
|
+
null);
|
|
44
|
+
});
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'processing': {
|
|
48
|
+
found = await withReadLock(ctx.processingLocks[location.shardIdx], () => {
|
|
49
|
+
return ctx.processingShards[location.shardIdx].get(jobId) ?? null;
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case 'completed':
|
|
54
|
+
found = ctx.storage?.getJob(jobId) ?? ctx.completedJobsData.get(jobId) ?? null;
|
|
55
|
+
break;
|
|
56
|
+
case 'dlq': {
|
|
57
|
+
if (ctx.storage) {
|
|
58
|
+
const dlqEntry = ctx.storage.getDlqEntry(jobId);
|
|
59
|
+
if (dlqEntry)
|
|
60
|
+
return dlqEntry.job;
|
|
61
|
+
const job = ctx.storage.getJob(jobId);
|
|
62
|
+
if (job)
|
|
63
|
+
return job;
|
|
64
|
+
}
|
|
65
|
+
const dlqShardIdx = shardIndex(location.queueName);
|
|
66
|
+
const dlqJobs = ctx.shards[dlqShardIdx].getDlq(location.queueName);
|
|
67
|
+
found = dlqJobs.find((j) => j.id === jobId) ?? null;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (found)
|
|
72
|
+
return found;
|
|
73
|
+
if (ctx.jobIndex.get(jobId) === location)
|
|
74
|
+
return null; // no move: genuine miss
|
|
75
|
+
// The job moved while we held the stale snapshot: chase it.
|
|
53
76
|
}
|
|
77
|
+
return null;
|
|
54
78
|
}
|
|
55
79
|
/** Get job result */
|
|
56
80
|
export function getJobResult(jobId, ctx) {
|
|
@@ -121,46 +145,56 @@ function resolveStateFromStorage(jobId, storage) {
|
|
|
121
145
|
}
|
|
122
146
|
/** Get job state by ID */
|
|
123
147
|
export async function getJobState(jobId, ctx) {
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
switch (location.type) {
|
|
133
|
-
case 'queue': {
|
|
134
|
-
// Check if job is delayed, waiting, or waiting for children/deps
|
|
135
|
-
const result = await withReadLock(ctx.shardLocks[location.shardIdx], () => {
|
|
136
|
-
const shard = ctx.shards[location.shardIdx];
|
|
137
|
-
const queueJob = shard.getQueue(location.queueName).find(jobId);
|
|
138
|
-
if (queueJob)
|
|
139
|
-
return { job: queueJob, waitingDeps: false, waitingChildren: false };
|
|
140
|
-
const depsJob = shard.waitingDeps.get(jobId);
|
|
141
|
-
if (depsJob)
|
|
142
|
-
return { job: depsJob, waitingDeps: true, waitingChildren: false };
|
|
143
|
-
const childrenJob = shard.waitingChildren.get(jobId);
|
|
144
|
-
if (childrenJob)
|
|
145
|
-
return { job: childrenJob, waitingDeps: false, waitingChildren: true };
|
|
146
|
-
return null;
|
|
147
|
-
});
|
|
148
|
-
if (!result)
|
|
149
|
-
return 'unknown';
|
|
150
|
-
if (result.waitingDeps || result.waitingChildren)
|
|
151
|
-
return 'waiting-children';
|
|
152
|
-
const now = Date.now();
|
|
153
|
-
if (result.job.runAt > now)
|
|
154
|
-
return "delayed" /* JobState.Delayed */;
|
|
155
|
-
return result.job.priority > 0 ? "prioritized" /* JobState.Prioritized */ : "waiting" /* JobState.Waiting */;
|
|
156
|
-
}
|
|
157
|
-
case 'processing':
|
|
158
|
-
return "active" /* JobState.Active */;
|
|
159
|
-
case 'completed':
|
|
148
|
+
// Same stale-snapshot chase as getJob: a 'queue' location read before the
|
|
149
|
+
// read-lock await may be outdated by a concurrent pull (queue -> processing)
|
|
150
|
+
// by the time the lookup runs. On a miss with a changed index entry, retry
|
|
151
|
+
// with the fresh location instead of reporting a false 'unknown'.
|
|
152
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
153
|
+
const location = ctx.jobIndex.get(jobId);
|
|
154
|
+
// Check completed set first (fast path)
|
|
155
|
+
if (ctx.completedJobs.has(jobId)) {
|
|
160
156
|
return "completed" /* JobState.Completed */;
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
}
|
|
158
|
+
if (!location) {
|
|
159
|
+
return resolveStateFromStorage(jobId, ctx.storage);
|
|
160
|
+
}
|
|
161
|
+
switch (location.type) {
|
|
162
|
+
case 'queue': {
|
|
163
|
+
// Check if job is delayed, waiting, or waiting for children/deps
|
|
164
|
+
const result = await withReadLock(ctx.shardLocks[location.shardIdx], () => {
|
|
165
|
+
const shard = ctx.shards[location.shardIdx];
|
|
166
|
+
const queueJob = shard.getQueue(location.queueName).find(jobId);
|
|
167
|
+
if (queueJob)
|
|
168
|
+
return { job: queueJob, waitingDeps: false, waitingChildren: false };
|
|
169
|
+
const depsJob = shard.waitingDeps.get(jobId);
|
|
170
|
+
if (depsJob)
|
|
171
|
+
return { job: depsJob, waitingDeps: true, waitingChildren: false };
|
|
172
|
+
const childrenJob = shard.waitingChildren.get(jobId);
|
|
173
|
+
if (childrenJob)
|
|
174
|
+
return { job: childrenJob, waitingDeps: false, waitingChildren: true };
|
|
175
|
+
return null;
|
|
176
|
+
});
|
|
177
|
+
if (!result) {
|
|
178
|
+
if (ctx.jobIndex.get(jobId) === location)
|
|
179
|
+
return 'unknown'; // no move
|
|
180
|
+
break; // moved mid-lookup: chase the new location
|
|
181
|
+
}
|
|
182
|
+
if (result.waitingDeps || result.waitingChildren)
|
|
183
|
+
return 'waiting-children';
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (result.job.runAt > now)
|
|
186
|
+
return "delayed" /* JobState.Delayed */;
|
|
187
|
+
return result.job.priority > 0 ? "prioritized" /* JobState.Prioritized */ : "waiting" /* JobState.Waiting */;
|
|
188
|
+
}
|
|
189
|
+
case 'processing':
|
|
190
|
+
return "active" /* JobState.Active */;
|
|
191
|
+
case 'completed':
|
|
192
|
+
return "completed" /* JobState.Completed */;
|
|
193
|
+
case 'dlq':
|
|
194
|
+
return "failed" /* JobState.Failed */;
|
|
195
|
+
}
|
|
163
196
|
}
|
|
197
|
+
return 'unknown';
|
|
164
198
|
}
|
|
165
199
|
/** Collect completed jobs for a queue from index + storage */
|
|
166
200
|
function collectCompletedJobs(queue, ctx, maxCollect) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunqueue",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.31",
|
|
4
4
|
"description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|