@strav/queue 0.4.31 → 1.0.0-alpha.4
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 +243 -0
- package/package.json +21 -29
- package/src/console/index.ts +10 -0
- package/src/console/queue_console_provider.ts +32 -0
- package/src/console/queue_failed.ts +57 -0
- package/src/console/queue_flush.ts +41 -0
- package/src/console/queue_retry.ts +68 -0
- package/src/console/queue_work.ts +85 -0
- package/src/console/scheduler_list.ts +27 -0
- package/src/console/scheduler_run.ts +31 -0
- package/src/console/scheduler_work.ts +32 -0
- package/src/cron.ts +194 -0
- package/src/database_queue.ts +177 -0
- package/src/failed_jobs_schema.ts +37 -0
- package/src/index.ts +62 -3
- package/src/job.ts +153 -0
- package/src/job_registry.ts +135 -0
- package/src/job_schema.ts +49 -0
- package/src/queue.ts +69 -0
- package/src/scheduler.ts +271 -0
- package/src/scheduler_runs_schema.ts +33 -0
- package/src/sync_queue.ts +126 -0
- package/src/worker.ts +351 -0
- package/src/providers/index.ts +0 -3
- package/src/providers/queue_provider.ts +0 -29
- package/src/queue/circuit_breaker.ts +0 -135
- package/src/queue/index.ts +0 -22
- package/src/queue/queue.ts +0 -493
- package/src/queue/worker.ts +0 -273
- package/src/scheduler/cron.ts +0 -146
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/runner.ts +0 -116
- package/src/scheduler/schedule.ts +0 -292
- package/src/scheduler/scheduler.ts +0 -71
- package/tsconfig.json +0 -5
package/src/queue/queue.ts
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
import { inject } from '@strav/kernel/core/inject'
|
|
2
|
-
import Configuration from '@strav/kernel/config/configuration'
|
|
3
|
-
import Database from '@strav/database/database/database'
|
|
4
|
-
import Emitter from '@strav/kernel/events/emitter'
|
|
5
|
-
import { ConfigurationError } from '@strav/kernel/exceptions/errors'
|
|
6
|
-
import { configureBreaker, type CircuitBreakerOptions } from './circuit_breaker.ts'
|
|
7
|
-
|
|
8
|
-
export interface JobOptions {
|
|
9
|
-
queue?: string
|
|
10
|
-
delay?: number
|
|
11
|
-
attempts?: number
|
|
12
|
-
timeout?: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface QueueConfig {
|
|
16
|
-
default: string
|
|
17
|
-
maxAttempts: number
|
|
18
|
-
timeout: number
|
|
19
|
-
retryBackoff: 'exponential' | 'linear'
|
|
20
|
-
sleep: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Metadata passed to job handlers alongside the payload. */
|
|
24
|
-
export interface JobMeta {
|
|
25
|
-
id: number
|
|
26
|
-
queue: string
|
|
27
|
-
job: string
|
|
28
|
-
attempts: number
|
|
29
|
-
maxAttempts: number
|
|
30
|
-
/**
|
|
31
|
-
* Report progress for a long-running job. `value` is `0..1`. The reported
|
|
32
|
-
* value is persisted to the job row so external consumers can poll via
|
|
33
|
-
* {@link Queue.progressOf}, and a `queue:progress` event is emitted for
|
|
34
|
-
* live consumers (e.g. SSE).
|
|
35
|
-
*
|
|
36
|
-
* Returns immediately after persisting; safe to call from a tight loop
|
|
37
|
-
* but throttle to avoid hammering the database (e.g. every N rows or
|
|
38
|
-
* every 1 s).
|
|
39
|
-
*/
|
|
40
|
-
progress: (value: number, message?: string) => Promise<void>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Snapshot of a job's current progress, returned by {@link Queue.progressOf}. */
|
|
44
|
-
export interface JobProgress {
|
|
45
|
-
/** Job id. */
|
|
46
|
-
id: number
|
|
47
|
-
/** 0..1, last reported by the handler. */
|
|
48
|
-
value: number
|
|
49
|
-
/** Optional human-readable message attached to the last update. */
|
|
50
|
-
message: string | null
|
|
51
|
-
/** Current attempt count. */
|
|
52
|
-
attempts: number
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** A raw job row from the _strav_jobs table. */
|
|
56
|
-
export interface JobRecord {
|
|
57
|
-
id: number
|
|
58
|
-
queue: string
|
|
59
|
-
job: string
|
|
60
|
-
payload: unknown
|
|
61
|
-
attempts: number
|
|
62
|
-
maxAttempts: number
|
|
63
|
-
timeout: number
|
|
64
|
-
availableAt: Date
|
|
65
|
-
reservedAt: Date | null
|
|
66
|
-
createdAt: Date
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** A raw row from the _strav_failed_jobs table. */
|
|
70
|
-
export interface FailedJobRecord {
|
|
71
|
-
id: number
|
|
72
|
-
queue: string
|
|
73
|
-
job: string
|
|
74
|
-
payload: unknown
|
|
75
|
-
error: string
|
|
76
|
-
failedAt: Date
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type JobHandler<T = any> = (payload: T, meta: JobMeta) => void | Promise<void>
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Minimal "schema-like" shape — anything that exposes `parse(input)`
|
|
83
|
-
* (Zod, ArkType, Valibot, hand-written validators) works. The schema
|
|
84
|
-
* is invoked at dequeue time, BEFORE the handler runs, so a tampered
|
|
85
|
-
* row in the DB or a payload from an older code revision is rejected
|
|
86
|
-
* loudly instead of executing with a half-formed shape.
|
|
87
|
-
*/
|
|
88
|
-
export interface JobPayloadSchema<T = unknown> {
|
|
89
|
-
parse(input: unknown): T
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Per-handler registration options. */
|
|
93
|
-
export interface JobHandlerOptions<T = any> {
|
|
94
|
-
/**
|
|
95
|
-
* Optional payload schema. When set, the worker calls `schema.parse(payload)`
|
|
96
|
-
* before invoking the handler; a parse failure routes the job to
|
|
97
|
-
* `_strav_failed_jobs` with the validation error message.
|
|
98
|
-
*
|
|
99
|
-
* Recommended for any handler whose payload comes from an external
|
|
100
|
-
* source (HTTP webhook, customer upload) or whose code has churned
|
|
101
|
-
* since older jobs were enqueued — the parse is a fail-fast invariant
|
|
102
|
-
* that catches drift before the handler corrupts state.
|
|
103
|
-
*/
|
|
104
|
-
schema?: JobPayloadSchema<T>
|
|
105
|
-
/**
|
|
106
|
-
* Per-handler circuit breaker. Trips when the failure count within
|
|
107
|
-
* `windowMs` reaches `threshold`, pausing dispatch for `cooldownMs`.
|
|
108
|
-
* Defends against retry storms — a stale-schema or downed-dependency
|
|
109
|
-
* handler shouldn't keep eating worker cycles. Defaults: threshold
|
|
110
|
-
* 10, windowMs 60_000, cooldownMs 30_000.
|
|
111
|
-
*/
|
|
112
|
-
circuitBreaker?: CircuitBreakerOptions
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Internal registration record stored in Queue._handlers. */
|
|
116
|
-
export interface JobHandlerRegistration<T = any> {
|
|
117
|
-
handler: JobHandler<T>
|
|
118
|
-
schema?: JobPayloadSchema<T>
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* PostgreSQL-backed job queue.
|
|
123
|
-
*
|
|
124
|
-
* Resolved once via the DI container — stores the database reference
|
|
125
|
-
* and parsed config for Worker and all static methods.
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* app.singleton(Queue)
|
|
129
|
-
* app.resolve(Queue)
|
|
130
|
-
* await Queue.ensureTables()
|
|
131
|
-
*
|
|
132
|
-
* Queue.handle('send-email', async (payload) => { ... })
|
|
133
|
-
* await Queue.push('send-email', { to: 'user@example.com' })
|
|
134
|
-
*/
|
|
135
|
-
@inject
|
|
136
|
-
export default class Queue {
|
|
137
|
-
private static _db: Database
|
|
138
|
-
private static _config: QueueConfig
|
|
139
|
-
private static _handlers = new Map<string, JobHandlerRegistration>()
|
|
140
|
-
|
|
141
|
-
constructor(db: Database, config: Configuration) {
|
|
142
|
-
Queue._db = db
|
|
143
|
-
Queue._config = {
|
|
144
|
-
default: config.get('queue.default', 'default') as string,
|
|
145
|
-
maxAttempts: config.get('queue.maxAttempts', 3) as number,
|
|
146
|
-
timeout: config.get('queue.timeout', 60_000) as number,
|
|
147
|
-
retryBackoff: config.get('queue.retryBackoff', 'exponential') as 'exponential' | 'linear',
|
|
148
|
-
sleep: config.get('queue.sleep', 1000) as number,
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
static get db(): Database {
|
|
153
|
-
if (!Queue._db)
|
|
154
|
-
throw new ConfigurationError(
|
|
155
|
-
'Queue not configured. Resolve Queue through the container first.'
|
|
156
|
-
)
|
|
157
|
-
return Queue._db
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
static get config(): QueueConfig {
|
|
161
|
-
return Queue._config
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
static get handlers(): Map<string, JobHandlerRegistration> {
|
|
165
|
-
return Queue._handlers
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Create the internal jobs and failed_jobs tables if they don't exist. */
|
|
169
|
-
static async ensureTables(): Promise<void> {
|
|
170
|
-
const sql = Queue.db.sql
|
|
171
|
-
|
|
172
|
-
await sql`
|
|
173
|
-
CREATE TABLE IF NOT EXISTS "_strav_jobs" (
|
|
174
|
-
"id" BIGSERIAL PRIMARY KEY,
|
|
175
|
-
"queue" VARCHAR(255) NOT NULL DEFAULT 'default',
|
|
176
|
-
"job" VARCHAR(255) NOT NULL,
|
|
177
|
-
"payload" JSONB NOT NULL DEFAULT '{}',
|
|
178
|
-
"attempts" INT NOT NULL DEFAULT 0,
|
|
179
|
-
"max_attempts" INT NOT NULL DEFAULT 3,
|
|
180
|
-
"timeout" INT NOT NULL DEFAULT 60000,
|
|
181
|
-
"available_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
182
|
-
"reserved_at" TIMESTAMPTZ,
|
|
183
|
-
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
184
|
-
"progress" NUMERIC NOT NULL DEFAULT 0,
|
|
185
|
-
"progress_message" TEXT
|
|
186
|
-
)
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
// Additive migrations for progress columns — for tables that existed
|
|
190
|
-
// before progress reporting was introduced.
|
|
191
|
-
await sql`
|
|
192
|
-
ALTER TABLE "_strav_jobs"
|
|
193
|
-
ADD COLUMN IF NOT EXISTS "progress" NUMERIC NOT NULL DEFAULT 0
|
|
194
|
-
`
|
|
195
|
-
await sql`
|
|
196
|
-
ALTER TABLE "_strav_jobs"
|
|
197
|
-
ADD COLUMN IF NOT EXISTS "progress_message" TEXT
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
await sql`
|
|
201
|
-
CREATE INDEX IF NOT EXISTS "idx_strav_jobs_queue_available"
|
|
202
|
-
ON "_strav_jobs" ("queue", "available_at")
|
|
203
|
-
WHERE "reserved_at" IS NULL
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
await sql`
|
|
207
|
-
CREATE TABLE IF NOT EXISTS "_strav_failed_jobs" (
|
|
208
|
-
"id" BIGSERIAL PRIMARY KEY,
|
|
209
|
-
"queue" VARCHAR(255) NOT NULL,
|
|
210
|
-
"job" VARCHAR(255) NOT NULL,
|
|
211
|
-
"payload" JSONB NOT NULL DEFAULT '{}',
|
|
212
|
-
"error" TEXT NOT NULL,
|
|
213
|
-
"failed_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
214
|
-
)
|
|
215
|
-
`
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Register a handler for a named job. Pass `options.schema` to have
|
|
220
|
-
* the worker validate the payload (Zod / ArkType / etc.) before
|
|
221
|
-
* invoking the handler — a parse failure routes the job to
|
|
222
|
-
* `_strav_failed_jobs` instead of running the handler with bad data.
|
|
223
|
-
*
|
|
224
|
-
* @example
|
|
225
|
-
* import { z } from 'zod'
|
|
226
|
-
* Queue.handle('send-email', async (payload) => { ... }, {
|
|
227
|
-
* schema: z.object({ to: z.string().email(), subject: z.string() }),
|
|
228
|
-
* })
|
|
229
|
-
*/
|
|
230
|
-
static handle<T = any>(
|
|
231
|
-
name: string,
|
|
232
|
-
handler: JobHandler<T>,
|
|
233
|
-
options?: JobHandlerOptions<T>
|
|
234
|
-
): void {
|
|
235
|
-
Queue._handlers.set(name, { handler, schema: options?.schema })
|
|
236
|
-
if (options?.circuitBreaker) {
|
|
237
|
-
configureBreaker(name, options.circuitBreaker)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Push a job onto the queue. Returns the job ID.
|
|
243
|
-
*/
|
|
244
|
-
static async push<T = any>(name: string, payload: T, options?: JobOptions): Promise<number> {
|
|
245
|
-
const sql = Queue.db.sql
|
|
246
|
-
const queue = options?.queue ?? Queue._config.default
|
|
247
|
-
const maxAttempts = options?.attempts ?? Queue._config.maxAttempts
|
|
248
|
-
const timeout = options?.timeout ?? Queue._config.timeout
|
|
249
|
-
const availableAt = options?.delay ? new Date(Date.now() + options.delay) : new Date()
|
|
250
|
-
|
|
251
|
-
const rows = await sql`
|
|
252
|
-
INSERT INTO "_strav_jobs" ("queue", "job", "payload", "max_attempts", "timeout", "available_at")
|
|
253
|
-
VALUES (${queue}, ${name}, ${JSON.stringify(payload)}, ${maxAttempts}, ${timeout}, ${availableAt})
|
|
254
|
-
RETURNING "id"
|
|
255
|
-
`
|
|
256
|
-
|
|
257
|
-
const id = Number((rows[0] as Record<string, unknown>).id)
|
|
258
|
-
|
|
259
|
-
if (Emitter.listenerCount('queue:dispatched') > 0) {
|
|
260
|
-
Emitter.emit('queue:dispatched', { id, name, queue, payload }).catch(() => {})
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return id
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Persist progress for an in-flight job and emit a `queue:progress` event.
|
|
268
|
-
* Called by the `JobMeta.progress` callback that workers hand to handlers,
|
|
269
|
-
* but exposed statically so other code (e.g. retry replay tools) can update
|
|
270
|
-
* progress directly. `value` is clamped to `[0, 1]`.
|
|
271
|
-
*/
|
|
272
|
-
static async reportProgress(
|
|
273
|
-
id: number,
|
|
274
|
-
value: number,
|
|
275
|
-
message?: string
|
|
276
|
-
): Promise<void> {
|
|
277
|
-
const sql = Queue.db.sql
|
|
278
|
-
const clamped = Math.max(0, Math.min(1, value))
|
|
279
|
-
const msg = message ?? null
|
|
280
|
-
await sql`
|
|
281
|
-
UPDATE "_strav_jobs"
|
|
282
|
-
SET "progress" = ${clamped}, "progress_message" = ${msg}
|
|
283
|
-
WHERE "id" = ${id}
|
|
284
|
-
`
|
|
285
|
-
if (Emitter.listenerCount('queue:progress') > 0) {
|
|
286
|
-
Emitter.emit('queue:progress', {
|
|
287
|
-
id,
|
|
288
|
-
value: clamped,
|
|
289
|
-
message: msg,
|
|
290
|
-
}).catch(() => {})
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Read the latest progress snapshot for a job. Returns `null` once the
|
|
296
|
-
* job has completed (the row is deleted on success) or if the id is
|
|
297
|
-
* unknown.
|
|
298
|
-
*/
|
|
299
|
-
static async progressOf(id: number): Promise<JobProgress | null> {
|
|
300
|
-
const sql = Queue.db.sql
|
|
301
|
-
const rows = await sql`
|
|
302
|
-
SELECT "id", "progress", "progress_message", "attempts"
|
|
303
|
-
FROM "_strav_jobs"
|
|
304
|
-
WHERE "id" = ${id}
|
|
305
|
-
LIMIT 1
|
|
306
|
-
`
|
|
307
|
-
if (rows.length === 0) return null
|
|
308
|
-
const row = rows[0] as Record<string, unknown>
|
|
309
|
-
return {
|
|
310
|
-
id: Number(row.id),
|
|
311
|
-
value: Number(row.progress ?? 0),
|
|
312
|
-
message: (row.progress_message as string | null) ?? null,
|
|
313
|
-
attempts: Number(row.attempts ?? 0),
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Create a listener function suitable for Emitter.on().
|
|
319
|
-
* When the event fires, the payload is pushed onto the queue.
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* Emitter.on('user.registered', Queue.listener('send-welcome-email'))
|
|
323
|
-
*/
|
|
324
|
-
static listener(jobName: string, options?: JobOptions): (payload: any) => Promise<void> {
|
|
325
|
-
return async (payload: any) => {
|
|
326
|
-
await Queue.push(jobName, payload, options)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
// Introspection / Management
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
|
|
334
|
-
/** Count of pending (unreserved) jobs in a queue. */
|
|
335
|
-
static async size(queue?: string): Promise<number> {
|
|
336
|
-
const sql = Queue.db.sql
|
|
337
|
-
const q = queue ?? Queue._config.default
|
|
338
|
-
|
|
339
|
-
const rows = await sql`
|
|
340
|
-
SELECT COUNT(*)::int AS count FROM "_strav_jobs"
|
|
341
|
-
WHERE "queue" = ${q} AND "reserved_at" IS NULL
|
|
342
|
-
`
|
|
343
|
-
return (rows[0] as Record<string, unknown>).count as number
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/** List pending jobs, most recent first. */
|
|
347
|
-
static async pending(queue?: string, limit = 25): Promise<JobRecord[]> {
|
|
348
|
-
const sql = Queue.db.sql
|
|
349
|
-
const q = queue ?? Queue._config.default
|
|
350
|
-
|
|
351
|
-
const rows = await sql`
|
|
352
|
-
SELECT * FROM "_strav_jobs"
|
|
353
|
-
WHERE "queue" = ${q}
|
|
354
|
-
ORDER BY "available_at" ASC
|
|
355
|
-
LIMIT ${limit}
|
|
356
|
-
`
|
|
357
|
-
return rows.map(hydrateJob)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** List failed jobs, most recent first. */
|
|
361
|
-
static async failed(queue?: string, limit = 25): Promise<FailedJobRecord[]> {
|
|
362
|
-
const sql = Queue.db.sql
|
|
363
|
-
|
|
364
|
-
if (queue) {
|
|
365
|
-
const rows = await sql`
|
|
366
|
-
SELECT * FROM "_strav_failed_jobs"
|
|
367
|
-
WHERE "queue" = ${queue}
|
|
368
|
-
ORDER BY "failed_at" DESC
|
|
369
|
-
LIMIT ${limit}
|
|
370
|
-
`
|
|
371
|
-
return rows.map(hydrateFailedJob)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const rows = await sql`
|
|
375
|
-
SELECT * FROM "_strav_failed_jobs"
|
|
376
|
-
ORDER BY "failed_at" DESC
|
|
377
|
-
LIMIT ${limit}
|
|
378
|
-
`
|
|
379
|
-
return rows.map(hydrateFailedJob)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Delete all pending jobs in a queue. Returns the number deleted. */
|
|
383
|
-
static async clear(queue?: string): Promise<number> {
|
|
384
|
-
const sql = Queue.db.sql
|
|
385
|
-
const q = queue ?? Queue._config.default
|
|
386
|
-
|
|
387
|
-
const rows = await sql`
|
|
388
|
-
DELETE FROM "_strav_jobs" WHERE "queue" = ${q}
|
|
389
|
-
`
|
|
390
|
-
return rows.count
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Move all failed jobs back to the jobs table. Returns the number retried. */
|
|
394
|
-
static async retryFailed(queue?: string): Promise<number> {
|
|
395
|
-
const sql = Queue.db.sql
|
|
396
|
-
|
|
397
|
-
if (queue) {
|
|
398
|
-
const failed = await sql`
|
|
399
|
-
DELETE FROM "_strav_failed_jobs" WHERE "queue" = ${queue} RETURNING *
|
|
400
|
-
`
|
|
401
|
-
for (const row of failed) {
|
|
402
|
-
const r = row as Record<string, unknown>
|
|
403
|
-
await sql`
|
|
404
|
-
INSERT INTO "_strav_jobs" ("queue", "job", "payload", "max_attempts")
|
|
405
|
-
VALUES (${r.queue}, ${r.job}, ${typeof r.payload === 'string' ? r.payload : JSON.stringify(r.payload)}, ${Queue._config.maxAttempts})
|
|
406
|
-
`
|
|
407
|
-
}
|
|
408
|
-
return failed.length
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const failed = await sql`
|
|
412
|
-
DELETE FROM "_strav_failed_jobs" RETURNING *
|
|
413
|
-
`
|
|
414
|
-
for (const row of failed) {
|
|
415
|
-
const r = row as Record<string, unknown>
|
|
416
|
-
await sql`
|
|
417
|
-
INSERT INTO "_strav_jobs" ("queue", "job", "payload", "max_attempts")
|
|
418
|
-
VALUES (${r.queue}, ${r.job}, ${typeof r.payload === 'string' ? r.payload : JSON.stringify(r.payload)}, ${Queue._config.maxAttempts})
|
|
419
|
-
`
|
|
420
|
-
}
|
|
421
|
-
return failed.length
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/** Delete all failed jobs for a queue (or all queues). Returns the number deleted. */
|
|
425
|
-
static async clearFailed(queue?: string): Promise<number> {
|
|
426
|
-
const sql = Queue.db.sql
|
|
427
|
-
|
|
428
|
-
if (queue) {
|
|
429
|
-
const rows = await sql`
|
|
430
|
-
DELETE FROM "_strav_failed_jobs" WHERE "queue" = ${queue}
|
|
431
|
-
`
|
|
432
|
-
return rows.count
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const rows = await sql`
|
|
436
|
-
DELETE FROM "_strav_failed_jobs"
|
|
437
|
-
`
|
|
438
|
-
return rows.count
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/** Delete all jobs and failed jobs across all queues. For dev/test only. */
|
|
442
|
-
static async flush(): Promise<void> {
|
|
443
|
-
const sql = Queue.db.sql
|
|
444
|
-
await sql`DELETE FROM "_strav_jobs"`
|
|
445
|
-
await sql`DELETE FROM "_strav_failed_jobs"`
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** Reset static state. For testing only. */
|
|
449
|
-
static reset(): void {
|
|
450
|
-
Queue._handlers.clear()
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
|
-
// Hydration helpers
|
|
456
|
-
// ---------------------------------------------------------------------------
|
|
457
|
-
|
|
458
|
-
function parsePayload(raw: unknown): unknown {
|
|
459
|
-
if (typeof raw === 'string') {
|
|
460
|
-
try {
|
|
461
|
-
return JSON.parse(raw)
|
|
462
|
-
} catch {
|
|
463
|
-
return raw
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return raw
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function hydrateJob(row: Record<string, unknown>): JobRecord {
|
|
470
|
-
return {
|
|
471
|
-
id: Number(row.id),
|
|
472
|
-
queue: row.queue as string,
|
|
473
|
-
job: row.job as string,
|
|
474
|
-
payload: parsePayload(row.payload),
|
|
475
|
-
attempts: row.attempts as number,
|
|
476
|
-
maxAttempts: row.max_attempts as number,
|
|
477
|
-
timeout: row.timeout as number,
|
|
478
|
-
availableAt: row.available_at as Date,
|
|
479
|
-
reservedAt: (row.reserved_at as Date) ?? null,
|
|
480
|
-
createdAt: row.created_at as Date,
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function hydrateFailedJob(row: Record<string, unknown>): FailedJobRecord {
|
|
485
|
-
return {
|
|
486
|
-
id: Number(row.id),
|
|
487
|
-
queue: row.queue as string,
|
|
488
|
-
job: row.job as string,
|
|
489
|
-
payload: parsePayload(row.payload),
|
|
490
|
-
error: row.error as string,
|
|
491
|
-
failedAt: row.failed_at as Date,
|
|
492
|
-
}
|
|
493
|
-
}
|