@happyvertical/jobs 0.80.0 → 0.80.2
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/dist/adapters/bull.js +343 -346
- package/dist/adapters/bull.js.map +1 -1
- package/dist/adapters/bullmq.js +351 -388
- package/dist/adapters/bullmq.js.map +1 -1
- package/dist/adapters/cloud-tasks.js +307 -333
- package/dist/adapters/cloud-tasks.js.map +1 -1
- package/dist/adapters/postgres.js +288 -328
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.js +236 -258
- package/dist/adapters/sqlite.js.map +1 -1
- package/dist/adapters/sqs.js +363 -408
- package/dist/adapters/sqs.js.map +1 -1
- package/dist/chunks/base-store-DIasEzL0.js +386 -0
- package/dist/chunks/base-store-DIasEzL0.js.map +1 -0
- package/dist/cli/claude-context.js +17 -17
- package/dist/cli/claude-context.js.map +1 -1
- package/dist/index.js +219 -242
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/chunks/base-store-DlNksWvQ.js +0 -324
- package/dist/chunks/base-store-DlNksWvQ.js.map +0 -1
|
@@ -1,28 +1,36 @@
|
|
|
1
|
+
import { r as validateTableName, t as BaseJobStore } from "../chunks/base-store-DIasEzL0.js";
|
|
1
2
|
import { getDatabase } from "@happyvertical/sql";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
//#region src/adapters/postgres.ts
|
|
4
|
+
/**
|
|
5
|
+
* PostgreSQL-based job store with NOTIFY/LISTEN support
|
|
6
|
+
*
|
|
7
|
+
* Uses PostgreSQL for job persistence with optional push-based
|
|
8
|
+
* notifications via NOTIFY/LISTEN for efficient job retrieval.
|
|
9
|
+
*/
|
|
10
|
+
var PostgresJobStore = class extends BaseJobStore {
|
|
11
|
+
db = null;
|
|
12
|
+
url;
|
|
13
|
+
externalDb;
|
|
14
|
+
tableName;
|
|
15
|
+
enableNotify;
|
|
16
|
+
notifyChannel;
|
|
17
|
+
notifyListeners = /* @__PURE__ */ new Set();
|
|
18
|
+
listening = false;
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.url = config.url ?? process.env.DATABASE_URL ?? "";
|
|
22
|
+
this.externalDb = config.db ?? null;
|
|
23
|
+
this.tableName = validateTableName(config.tableName ?? "_jobs");
|
|
24
|
+
this.enableNotify = config.enableNotify ?? true;
|
|
25
|
+
this.notifyChannel = validateTableName(config.notifyChannel ?? "job_events");
|
|
26
|
+
}
|
|
27
|
+
async initialize() {
|
|
28
|
+
if (this.initialized) return;
|
|
29
|
+
this.db = this.externalDb ?? await getDatabase({
|
|
30
|
+
type: "postgres",
|
|
31
|
+
url: this.url
|
|
32
|
+
});
|
|
33
|
+
await this.db.query(`
|
|
26
34
|
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
27
35
|
id TEXT PRIMARY KEY,
|
|
28
36
|
queue TEXT NOT NULL DEFAULT 'default',
|
|
@@ -45,27 +53,25 @@ class PostgresJobStore extends BaseJobStore {
|
|
|
45
53
|
updated_at TIMESTAMPTZ NOT NULL
|
|
46
54
|
)
|
|
47
55
|
`);
|
|
48
|
-
|
|
56
|
+
await this.db.query(`
|
|
49
57
|
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_dequeue
|
|
50
58
|
ON ${this.tableName} (status, queue, run_at, priority DESC)
|
|
51
59
|
WHERE status = 'pending'
|
|
52
60
|
`);
|
|
53
|
-
|
|
61
|
+
await this.db.query(`
|
|
54
62
|
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
|
|
55
63
|
ON ${this.tableName} (created_at)
|
|
56
64
|
`);
|
|
57
|
-
|
|
65
|
+
await this.db.query(`
|
|
58
66
|
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_queue
|
|
59
67
|
ON ${this.tableName} (queue)
|
|
60
68
|
`);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!this.db) return;
|
|
68
|
-
await this.db.query(`
|
|
69
|
+
if (this.enableNotify) await this.setupNotifyTriggers();
|
|
70
|
+
this.initialized = true;
|
|
71
|
+
}
|
|
72
|
+
async setupNotifyTriggers() {
|
|
73
|
+
if (!this.db) return;
|
|
74
|
+
await this.db.query(`
|
|
69
75
|
CREATE OR REPLACE FUNCTION ${this.tableName}_notify_created()
|
|
70
76
|
RETURNS TRIGGER AS $$
|
|
71
77
|
BEGIN
|
|
@@ -80,15 +86,13 @@ class PostgresJobStore extends BaseJobStore {
|
|
|
80
86
|
END;
|
|
81
87
|
$$ LANGUAGE plpgsql
|
|
82
88
|
`);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
await this.db.query(`
|
|
89
|
+
await this.db.query(`DROP TRIGGER IF EXISTS ${this.tableName}_created_trigger ON ${this.tableName}`);
|
|
90
|
+
await this.db.query(`
|
|
87
91
|
CREATE TRIGGER ${this.tableName}_created_trigger
|
|
88
92
|
AFTER INSERT ON ${this.tableName}
|
|
89
93
|
FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_created()
|
|
90
94
|
`);
|
|
91
|
-
|
|
95
|
+
await this.db.query(`
|
|
92
96
|
CREATE OR REPLACE FUNCTION ${this.tableName}_notify_ready()
|
|
93
97
|
RETURNS TRIGGER AS $$
|
|
94
98
|
BEGIN
|
|
@@ -105,72 +109,66 @@ class PostgresJobStore extends BaseJobStore {
|
|
|
105
109
|
END;
|
|
106
110
|
$$ LANGUAGE plpgsql
|
|
107
111
|
`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
111
|
-
await this.db.query(`
|
|
112
|
+
await this.db.query(`DROP TRIGGER IF EXISTS ${this.tableName}_ready_trigger ON ${this.tableName}`);
|
|
113
|
+
await this.db.query(`
|
|
112
114
|
CREATE TRIGGER ${this.tableName}_ready_trigger
|
|
113
115
|
AFTER UPDATE ON ${this.tableName}
|
|
114
116
|
FOR EACH ROW EXECUTE FUNCTION ${this.tableName}_notify_ready()
|
|
115
117
|
`);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
-
const queuePlaceholders = queues.map((_, i) => `$${i + 1}`).join(", ");
|
|
172
|
-
const { rows } = await this.db.query(
|
|
173
|
-
`
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Start listening for PostgreSQL notifications
|
|
121
|
+
* This enables push-based job retrieval
|
|
122
|
+
*/
|
|
123
|
+
async startListening() {
|
|
124
|
+
if (!this.db || !this.enableNotify || this.listening) return;
|
|
125
|
+
try {
|
|
126
|
+
await this.db.query(`LISTEN ${this.notifyChannel}`);
|
|
127
|
+
this.listening = true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn("Could not start LISTEN, falling back to polling:", error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async stopListening() {
|
|
133
|
+
if (!this.db || !this.listening) return;
|
|
134
|
+
try {
|
|
135
|
+
await this.db.query(`UNLISTEN ${this.notifyChannel}`);
|
|
136
|
+
this.listening = false;
|
|
137
|
+
} catch (error) {}
|
|
138
|
+
}
|
|
139
|
+
async enqueue(options) {
|
|
140
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
141
|
+
const job = this.createJobRecord(options);
|
|
142
|
+
await this.db.insert(this.tableName, {
|
|
143
|
+
id: job.id,
|
|
144
|
+
queue: job.queue,
|
|
145
|
+
payload: JSON.stringify(job.payload),
|
|
146
|
+
status: job.status,
|
|
147
|
+
priority: job.priority,
|
|
148
|
+
attempts: job.attempts,
|
|
149
|
+
max_attempts: job.maxAttempts,
|
|
150
|
+
run_at: job.runAt.toISOString(),
|
|
151
|
+
started_at: job.startedAt?.toISOString() ?? null,
|
|
152
|
+
completed_at: job.completedAt?.toISOString() ?? null,
|
|
153
|
+
timeout: job.timeout,
|
|
154
|
+
timeout_behavior: job.timeoutBehavior,
|
|
155
|
+
last_error: job.lastError,
|
|
156
|
+
result_pointer: job.resultPointer,
|
|
157
|
+
retry_strategy: JSON.stringify(job.retryStrategy),
|
|
158
|
+
worker_id: job.workerId,
|
|
159
|
+
worker_heartbeat: job.workerHeartbeat?.toISOString() ?? null,
|
|
160
|
+
created_at: job.createdAt.toISOString(),
|
|
161
|
+
updated_at: job.updatedAt.toISOString()
|
|
162
|
+
});
|
|
163
|
+
await this.emitEvent("job.created", job);
|
|
164
|
+
if (job.runAt <= /* @__PURE__ */ new Date()) await this.emitEvent("job.ready", job);
|
|
165
|
+
return job;
|
|
166
|
+
}
|
|
167
|
+
async dequeue(queues, limit, workerId) {
|
|
168
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
169
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
170
|
+
const queuePlaceholders = queues.map((_, i) => `$${i + 1}`).join(", ");
|
|
171
|
+
const { rows } = await this.db.query(`
|
|
174
172
|
UPDATE ${this.tableName}
|
|
175
173
|
SET status = 'running',
|
|
176
174
|
worker_id = $${queues.length + 1},
|
|
@@ -188,178 +186,149 @@ class PostgresJobStore extends BaseJobStore {
|
|
|
188
186
|
FOR UPDATE SKIP LOCKED
|
|
189
187
|
)
|
|
190
188
|
RETURNING *
|
|
191
|
-
`,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
async cleanup(options) {
|
|
337
|
-
if (!this.db) throw new Error("Store not initialized");
|
|
338
|
-
const conditions = [];
|
|
339
|
-
const params = [];
|
|
340
|
-
let paramIndex = 1;
|
|
341
|
-
if (options.completedBefore) {
|
|
342
|
-
conditions.push(
|
|
343
|
-
`(status = 'completed' AND completed_at < $${paramIndex++})`
|
|
344
|
-
);
|
|
345
|
-
params.push(options.completedBefore.toISOString());
|
|
346
|
-
}
|
|
347
|
-
if (options.failedBefore) {
|
|
348
|
-
conditions.push(
|
|
349
|
-
`(status = 'failed' AND completed_at < $${paramIndex++})`
|
|
350
|
-
);
|
|
351
|
-
params.push(options.failedBefore.toISOString());
|
|
352
|
-
}
|
|
353
|
-
if (options.cancelledBefore) {
|
|
354
|
-
conditions.push(
|
|
355
|
-
`(status = 'cancelled' AND completed_at < $${paramIndex++})`
|
|
356
|
-
);
|
|
357
|
-
params.push(options.cancelledBefore.toISOString());
|
|
358
|
-
}
|
|
359
|
-
if (conditions.length === 0) return 0;
|
|
360
|
-
let query = `DELETE FROM ${this.tableName} WHERE (${conditions.join(" OR ")})`;
|
|
361
|
-
if (options.limit) {
|
|
362
|
-
query = `
|
|
189
|
+
`, [
|
|
190
|
+
...queues,
|
|
191
|
+
workerId,
|
|
192
|
+
now,
|
|
193
|
+
limit
|
|
194
|
+
]);
|
|
195
|
+
const jobs = rows.map((row) => this.parseJobRow(row));
|
|
196
|
+
for (const job of jobs) await this.emitEvent("job.started", job);
|
|
197
|
+
return jobs;
|
|
198
|
+
}
|
|
199
|
+
async update(id, updates) {
|
|
200
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
201
|
+
const setClause = [];
|
|
202
|
+
const params = [];
|
|
203
|
+
let paramIndex = 1;
|
|
204
|
+
const fieldMap = {
|
|
205
|
+
queue: "queue",
|
|
206
|
+
payload: "payload",
|
|
207
|
+
status: "status",
|
|
208
|
+
priority: "priority",
|
|
209
|
+
attempts: "attempts",
|
|
210
|
+
maxAttempts: "max_attempts",
|
|
211
|
+
runAt: "run_at",
|
|
212
|
+
startedAt: "started_at",
|
|
213
|
+
completedAt: "completed_at",
|
|
214
|
+
timeout: "timeout",
|
|
215
|
+
timeoutBehavior: "timeout_behavior",
|
|
216
|
+
lastError: "last_error",
|
|
217
|
+
resultPointer: "result_pointer",
|
|
218
|
+
retryStrategy: "retry_strategy",
|
|
219
|
+
workerId: "worker_id",
|
|
220
|
+
workerHeartbeat: "worker_heartbeat"
|
|
221
|
+
};
|
|
222
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
223
|
+
const column = fieldMap[key];
|
|
224
|
+
if (!column) continue;
|
|
225
|
+
setClause.push(`${column} = $${paramIndex}`);
|
|
226
|
+
if (value instanceof Date) params.push(value.toISOString());
|
|
227
|
+
else if (typeof value === "object" && value !== null) params.push(JSON.stringify(value));
|
|
228
|
+
else params.push(value);
|
|
229
|
+
paramIndex++;
|
|
230
|
+
}
|
|
231
|
+
if (setClause.length === 0) {
|
|
232
|
+
const job = await this.get(id);
|
|
233
|
+
if (!job) throw new Error(`Job not found: ${id}`);
|
|
234
|
+
return job;
|
|
235
|
+
}
|
|
236
|
+
setClause.push(`updated_at = $${paramIndex}`);
|
|
237
|
+
params.push((/* @__PURE__ */ new Date()).toISOString());
|
|
238
|
+
paramIndex++;
|
|
239
|
+
params.push(id);
|
|
240
|
+
await this.db.query(`UPDATE ${this.tableName} SET ${setClause.join(", ")} WHERE id = $${paramIndex}`, params);
|
|
241
|
+
const job = await this.get(id);
|
|
242
|
+
if (!job) throw new Error(`Job not found after update: ${id}`);
|
|
243
|
+
if (updates.status === "completed") await this.emitEvent("job.completed", job, { resultPointer: job.resultPointer ?? void 0 });
|
|
244
|
+
else if (updates.status === "failed") await this.emitEvent("job.failed", job, { error: job.lastError ?? void 0 });
|
|
245
|
+
else if (updates.status === "cancelled") await this.emitEvent("job.cancelled", job);
|
|
246
|
+
return job;
|
|
247
|
+
}
|
|
248
|
+
async get(id) {
|
|
249
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
250
|
+
const row = await this.db.get(this.tableName, { id });
|
|
251
|
+
if (!row) return null;
|
|
252
|
+
return this.parseJobRow(row);
|
|
253
|
+
}
|
|
254
|
+
async list(filter) {
|
|
255
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
256
|
+
const conditions = [];
|
|
257
|
+
const params = [];
|
|
258
|
+
let paramIndex = 1;
|
|
259
|
+
if (filter.queue) {
|
|
260
|
+
conditions.push(`queue = $${paramIndex++}`);
|
|
261
|
+
params.push(filter.queue);
|
|
262
|
+
}
|
|
263
|
+
if (filter.status) if (Array.isArray(filter.status)) {
|
|
264
|
+
const placeholders = filter.status.map(() => `$${paramIndex++}`).join(", ");
|
|
265
|
+
conditions.push(`status IN (${placeholders})`);
|
|
266
|
+
params.push(...filter.status);
|
|
267
|
+
} else {
|
|
268
|
+
conditions.push(`status = $${paramIndex++}`);
|
|
269
|
+
params.push(filter.status);
|
|
270
|
+
}
|
|
271
|
+
if (filter.objectType) {
|
|
272
|
+
conditions.push(`payload->>'objectType' = $${paramIndex++}`);
|
|
273
|
+
params.push(filter.objectType);
|
|
274
|
+
}
|
|
275
|
+
if (filter.method) {
|
|
276
|
+
conditions.push(`payload->>'method' = $${paramIndex++}`);
|
|
277
|
+
params.push(filter.method);
|
|
278
|
+
}
|
|
279
|
+
if (filter.createdAfter) {
|
|
280
|
+
conditions.push(`created_at > $${paramIndex++}`);
|
|
281
|
+
params.push(filter.createdAfter.toISOString());
|
|
282
|
+
}
|
|
283
|
+
if (filter.createdBefore) {
|
|
284
|
+
conditions.push(`created_at < $${paramIndex++}`);
|
|
285
|
+
params.push(filter.createdBefore.toISOString());
|
|
286
|
+
}
|
|
287
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
288
|
+
const orderBy = this.buildOrderBy(filter);
|
|
289
|
+
let limitOffset = "";
|
|
290
|
+
if (filter.limit) {
|
|
291
|
+
limitOffset = `LIMIT $${paramIndex++}`;
|
|
292
|
+
params.push(filter.limit);
|
|
293
|
+
if (filter.offset) {
|
|
294
|
+
limitOffset += ` OFFSET $${paramIndex++}`;
|
|
295
|
+
params.push(filter.offset);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const { rows } = await this.db.query(`SELECT * FROM ${this.tableName} ${where} ${orderBy} ${limitOffset}`, params);
|
|
299
|
+
return rows.map((row) => this.parseJobRow(row));
|
|
300
|
+
}
|
|
301
|
+
async cancel(id) {
|
|
302
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
303
|
+
const job = await this.get(id);
|
|
304
|
+
if (!job) throw new Error(`Job not found: ${id}`);
|
|
305
|
+
if (job.status === "completed" || job.status === "cancelled") throw new Error(`Cannot cancel job with status: ${job.status}`);
|
|
306
|
+
await this.update(id, {
|
|
307
|
+
status: "cancelled",
|
|
308
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async cleanup(options) {
|
|
312
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
313
|
+
const conditions = [];
|
|
314
|
+
const params = [];
|
|
315
|
+
let paramIndex = 1;
|
|
316
|
+
if (options.completedBefore) {
|
|
317
|
+
conditions.push(`(status = 'completed' AND completed_at < $${paramIndex++})`);
|
|
318
|
+
params.push(options.completedBefore.toISOString());
|
|
319
|
+
}
|
|
320
|
+
if (options.failedBefore) {
|
|
321
|
+
conditions.push(`(status = 'failed' AND completed_at < $${paramIndex++})`);
|
|
322
|
+
params.push(options.failedBefore.toISOString());
|
|
323
|
+
}
|
|
324
|
+
if (options.cancelledBefore) {
|
|
325
|
+
conditions.push(`(status = 'cancelled' AND completed_at < $${paramIndex++})`);
|
|
326
|
+
params.push(options.cancelledBefore.toISOString());
|
|
327
|
+
}
|
|
328
|
+
if (conditions.length === 0) return 0;
|
|
329
|
+
let query = `DELETE FROM ${this.tableName} WHERE (${conditions.join(" OR ")})`;
|
|
330
|
+
if (options.limit) {
|
|
331
|
+
query = `
|
|
363
332
|
DELETE FROM ${this.tableName}
|
|
364
333
|
WHERE id IN (
|
|
365
334
|
SELECT id FROM ${this.tableName}
|
|
@@ -367,71 +336,62 @@ class PostgresJobStore extends BaseJobStore {
|
|
|
367
336
|
LIMIT $${paramIndex}
|
|
368
337
|
)
|
|
369
338
|
`;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await this.db.query(
|
|
379
|
-
`
|
|
339
|
+
params.push(options.limit);
|
|
340
|
+
}
|
|
341
|
+
return (await this.db.query(query, params)).rowCount ?? 0;
|
|
342
|
+
}
|
|
343
|
+
async heartbeat(jobId, workerId) {
|
|
344
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
345
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
346
|
+
await this.db.query(`
|
|
380
347
|
UPDATE ${this.tableName}
|
|
381
348
|
SET worker_heartbeat = $1, updated_at = $1
|
|
382
349
|
WHERE id = $2 AND worker_id = $3 AND status = 'running'
|
|
383
|
-
`,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
350
|
+
`, [
|
|
351
|
+
now,
|
|
352
|
+
jobId,
|
|
353
|
+
workerId
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
async stats(queue) {
|
|
357
|
+
if (!this.db) throw new Error("Store not initialized");
|
|
358
|
+
const params = [];
|
|
359
|
+
let paramIndex = 1;
|
|
360
|
+
const queueFilter = queue ? `WHERE queue = $${paramIndex++}` : "";
|
|
361
|
+
if (queue) params.push(queue);
|
|
362
|
+
const { rows: countRows } = await this.db.query(`
|
|
395
363
|
SELECT status, COUNT(*)::int as count
|
|
396
364
|
FROM ${this.tableName}
|
|
397
365
|
${queueFilter}
|
|
398
366
|
GROUP BY status
|
|
399
|
-
`,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
for (const row of countRows) {
|
|
404
|
-
counts[row.status] = row.count;
|
|
405
|
-
}
|
|
406
|
-
const { rows: durationRows } = await this.db.query(
|
|
407
|
-
`
|
|
367
|
+
`, params);
|
|
368
|
+
const counts = {};
|
|
369
|
+
for (const row of countRows) counts[row.status] = row.count;
|
|
370
|
+
const { rows: durationRows } = await this.db.query(`
|
|
408
371
|
SELECT AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000)::float as avg_duration
|
|
409
372
|
FROM ${this.tableName}
|
|
410
373
|
WHERE status = 'completed'
|
|
411
374
|
AND started_at IS NOT NULL
|
|
412
375
|
AND completed_at IS NOT NULL
|
|
413
376
|
${queue ? `AND queue = $1` : ""}
|
|
414
|
-
`,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.initialized = false;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
export {
|
|
434
|
-
PostgresJobStore,
|
|
435
|
-
PostgresJobStore as default
|
|
377
|
+
`, queue ? [queue] : []);
|
|
378
|
+
const avgDuration = durationRows[0]?.avg_duration ?? null;
|
|
379
|
+
return {
|
|
380
|
+
pending: counts["pending"] ?? 0,
|
|
381
|
+
running: counts["running"] ?? 0,
|
|
382
|
+
completed: counts["completed"] ?? 0,
|
|
383
|
+
failed: counts["failed"] ?? 0,
|
|
384
|
+
cancelled: counts["cancelled"] ?? 0,
|
|
385
|
+
avgDuration: avgDuration ? Math.round(avgDuration) : null
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async close() {
|
|
389
|
+
await this.stopListening();
|
|
390
|
+
this.db = null;
|
|
391
|
+
this.initialized = false;
|
|
392
|
+
}
|
|
436
393
|
};
|
|
437
|
-
//#
|
|
394
|
+
//#endregion
|
|
395
|
+
export { PostgresJobStore, PostgresJobStore as default };
|
|
396
|
+
|
|
397
|
+
//# sourceMappingURL=postgres.js.map
|