@bitclaw/jobs 1.1.0
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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/cron.d.ts +11 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +86 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/queue.d.ts +63 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +522 -0
- package/dist/rate-limiter.d.ts +11 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +27 -0
- package/dist/scheduler.d.ts +26 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +176 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +103 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +5 -0
- package/dist/worker.d.ts +21 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +111 -0
- package/package.json +67 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// packages/jobs/src/scheduler.ts
|
|
2
|
+
// Cron scheduler — hybrid register/upsert pattern for recurring jobs
|
|
3
|
+
import { nextCronOccurrence, parseCron } from './cron';
|
|
4
|
+
import { nowISO } from './utils';
|
|
5
|
+
function toSchedule(row) {
|
|
6
|
+
return {
|
|
7
|
+
id: row.id,
|
|
8
|
+
name: row.name,
|
|
9
|
+
type: row.type,
|
|
10
|
+
data: JSON.parse(row.data),
|
|
11
|
+
cron: row.cron,
|
|
12
|
+
timezone: row.timezone,
|
|
13
|
+
enabled: row.enabled === 1,
|
|
14
|
+
overlap: row.overlap === 1,
|
|
15
|
+
maxRetries: row.max_retries,
|
|
16
|
+
lastRunAt: row.last_run_at,
|
|
17
|
+
nextRunAt: row.next_run_at,
|
|
18
|
+
createdAt: row.created_at,
|
|
19
|
+
updatedAt: row.updated_at
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export class Scheduler {
|
|
23
|
+
queue;
|
|
24
|
+
timer = null;
|
|
25
|
+
upsertStmt;
|
|
26
|
+
selectByNameStmt;
|
|
27
|
+
selectAllStmt;
|
|
28
|
+
selectDueStmt;
|
|
29
|
+
updateLastRunStmt;
|
|
30
|
+
pauseStmt;
|
|
31
|
+
resumeStmt;
|
|
32
|
+
removeStmt;
|
|
33
|
+
constructor(queue) {
|
|
34
|
+
this.queue = queue;
|
|
35
|
+
const db = queue.db;
|
|
36
|
+
this.upsertStmt = db.query(`
|
|
37
|
+
INSERT INTO schedules (name, type, data, cron, timezone, overlap, max_retries, next_run_at, created_at, updated_at)
|
|
38
|
+
VALUES ($name, $type, $data, $cron, $timezone, $overlap, $maxRetries, $nextRunAt, $now, $now)
|
|
39
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
40
|
+
type = $type,
|
|
41
|
+
data = $data,
|
|
42
|
+
cron = $cron,
|
|
43
|
+
timezone = $timezone,
|
|
44
|
+
overlap = $overlap,
|
|
45
|
+
max_retries = $maxRetries,
|
|
46
|
+
next_run_at = CASE WHEN schedules.cron != $cron THEN $nextRunAt ELSE schedules.next_run_at END,
|
|
47
|
+
updated_at = $now
|
|
48
|
+
`);
|
|
49
|
+
this.selectByNameStmt = db.query('SELECT * FROM schedules WHERE name = $name');
|
|
50
|
+
this.selectAllStmt = db.query('SELECT * FROM schedules ORDER BY name');
|
|
51
|
+
this.selectDueStmt = db.query(`
|
|
52
|
+
SELECT * FROM schedules
|
|
53
|
+
WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= $now
|
|
54
|
+
`);
|
|
55
|
+
this.updateLastRunStmt = db.query(`
|
|
56
|
+
UPDATE schedules
|
|
57
|
+
SET last_run_at = $now, next_run_at = $nextRunAt, updated_at = $now
|
|
58
|
+
WHERE id = $id
|
|
59
|
+
`);
|
|
60
|
+
this.pauseStmt = db.query(`
|
|
61
|
+
UPDATE schedules SET enabled = 0, updated_at = $now WHERE name = $name
|
|
62
|
+
`);
|
|
63
|
+
this.resumeStmt = db.query(`
|
|
64
|
+
UPDATE schedules SET enabled = 1, updated_at = $now WHERE name = $name
|
|
65
|
+
`);
|
|
66
|
+
this.removeStmt = db.query('DELETE FROM schedules WHERE name = $name');
|
|
67
|
+
}
|
|
68
|
+
register(name, type, cron, options) {
|
|
69
|
+
const parsed = parseCron(cron);
|
|
70
|
+
const nextRunAt = nextCronOccurrence(parsed, new Date()).toISOString();
|
|
71
|
+
const now = nowISO();
|
|
72
|
+
this.upsertStmt.run({
|
|
73
|
+
$name: name,
|
|
74
|
+
$type: type,
|
|
75
|
+
$data: JSON.stringify(options?.data ?? {}),
|
|
76
|
+
$cron: cron,
|
|
77
|
+
$timezone: options?.timezone ?? 'UTC',
|
|
78
|
+
$overlap: options?.overlap ? 1 : 0,
|
|
79
|
+
$maxRetries: options?.maxRetries ?? 3,
|
|
80
|
+
$nextRunAt: nextRunAt,
|
|
81
|
+
$now: now
|
|
82
|
+
});
|
|
83
|
+
return this.getSchedule(name);
|
|
84
|
+
}
|
|
85
|
+
cleanup(registeredNames) {
|
|
86
|
+
if (registeredNames.length === 0) {
|
|
87
|
+
const result = this.queue.db.query('DELETE FROM schedules').run();
|
|
88
|
+
return result.changes;
|
|
89
|
+
}
|
|
90
|
+
// Build parameterized query for the IN clause
|
|
91
|
+
const placeholders = registeredNames.map((_, i) => `$p${i}`).join(', ');
|
|
92
|
+
const params = {};
|
|
93
|
+
for (let i = 0; i < registeredNames.length; i++) {
|
|
94
|
+
params[`$p${i}`] = registeredNames[i];
|
|
95
|
+
}
|
|
96
|
+
const result = this.queue.db
|
|
97
|
+
.query(`DELETE FROM schedules WHERE name NOT IN (${placeholders})`)
|
|
98
|
+
.run(params);
|
|
99
|
+
return result.changes;
|
|
100
|
+
}
|
|
101
|
+
pauseSchedule(name) {
|
|
102
|
+
this.pauseStmt.run({ $name: name, $now: nowISO() });
|
|
103
|
+
}
|
|
104
|
+
resumeSchedule(name) {
|
|
105
|
+
this.resumeStmt.run({ $name: name, $now: nowISO() });
|
|
106
|
+
}
|
|
107
|
+
getSchedules() {
|
|
108
|
+
const rows = this.selectAllStmt.all();
|
|
109
|
+
return rows.map(toSchedule);
|
|
110
|
+
}
|
|
111
|
+
getSchedule(name) {
|
|
112
|
+
const row = this.selectByNameStmt.get({
|
|
113
|
+
$name: name
|
|
114
|
+
});
|
|
115
|
+
return row ? toSchedule(row) : null;
|
|
116
|
+
}
|
|
117
|
+
removeSchedule(name) {
|
|
118
|
+
const result = this.removeStmt.run({ $name: name });
|
|
119
|
+
return result.changes > 0;
|
|
120
|
+
}
|
|
121
|
+
tick() {
|
|
122
|
+
const now = nowISO();
|
|
123
|
+
const dueSchedules = this.selectDueStmt.all({ $now: now });
|
|
124
|
+
let enqueued = 0;
|
|
125
|
+
for (const row of dueSchedules) {
|
|
126
|
+
// Overlap check: if overlap=false, skip if there's a pending/processing job of this type
|
|
127
|
+
if (row.overlap === 0) {
|
|
128
|
+
const active = this.queue.db
|
|
129
|
+
.query("SELECT 1 FROM jobs WHERE type = $type AND status IN ('pending', 'processing') LIMIT 1")
|
|
130
|
+
.get({ $type: row.type });
|
|
131
|
+
if (active) {
|
|
132
|
+
// Skip this tick, recalculate next_run_at
|
|
133
|
+
const parsed = parseCron(row.cron);
|
|
134
|
+
const nextRunAt = nextCronOccurrence(parsed, new Date()).toISOString();
|
|
135
|
+
this.updateLastRunStmt.run({
|
|
136
|
+
$id: row.id,
|
|
137
|
+
$now: now,
|
|
138
|
+
$nextRunAt: nextRunAt
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Enqueue the job
|
|
144
|
+
this.queue.db
|
|
145
|
+
.query(`INSERT INTO jobs (type, data, status, priority, max_retries, run_at, batch_id)
|
|
146
|
+
VALUES ($type, $data, 'pending', 0, $maxRetries, $now, NULL)`)
|
|
147
|
+
.run({
|
|
148
|
+
$type: row.type,
|
|
149
|
+
$data: row.data,
|
|
150
|
+
$maxRetries: row.max_retries,
|
|
151
|
+
$now: now
|
|
152
|
+
});
|
|
153
|
+
// Update schedule timestamps
|
|
154
|
+
const parsed = parseCron(row.cron);
|
|
155
|
+
const nextRunAt = nextCronOccurrence(parsed, new Date()).toISOString();
|
|
156
|
+
this.updateLastRunStmt.run({
|
|
157
|
+
$id: row.id,
|
|
158
|
+
$now: now,
|
|
159
|
+
$nextRunAt: nextRunAt
|
|
160
|
+
});
|
|
161
|
+
enqueued++;
|
|
162
|
+
}
|
|
163
|
+
return enqueued;
|
|
164
|
+
}
|
|
165
|
+
start(intervalMs = 60_000) {
|
|
166
|
+
if (this.timer)
|
|
167
|
+
return;
|
|
168
|
+
this.timer = setInterval(() => this.tick(), intervalMs);
|
|
169
|
+
}
|
|
170
|
+
stop() {
|
|
171
|
+
if (this.timer) {
|
|
172
|
+
clearInterval(this.timer);
|
|
173
|
+
this.timer = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA2F3C,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAY/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAUnD"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const JOBS_TABLE = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
3
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
type TEXT NOT NULL,
|
|
5
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
6
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
7
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
10
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
run_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
12
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
13
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
14
|
+
started_at TEXT,
|
|
15
|
+
completed_at TEXT,
|
|
16
|
+
error TEXT,
|
|
17
|
+
request_log TEXT,
|
|
18
|
+
response_log TEXT,
|
|
19
|
+
batch_id TEXT REFERENCES job_batches(id)
|
|
20
|
+
)`;
|
|
21
|
+
const JOBS_POLL_INDEX = `
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_poll
|
|
23
|
+
ON jobs (status, type, run_at, priority DESC, created_at ASC)`;
|
|
24
|
+
const JOBS_STATUS_INDEX = `
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs (status)`;
|
|
26
|
+
const JOB_DEPENDENCIES_TABLE = `
|
|
27
|
+
CREATE TABLE IF NOT EXISTS job_dependencies (
|
|
28
|
+
job_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
29
|
+
depends_on_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
30
|
+
PRIMARY KEY (job_id, depends_on_id)
|
|
31
|
+
)`;
|
|
32
|
+
const JOB_DEPS_INDEX = `
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_job_deps_depends_on
|
|
34
|
+
ON job_dependencies (depends_on_id)`;
|
|
35
|
+
const FAILED_JOBS_TABLE = `
|
|
36
|
+
CREATE TABLE IF NOT EXISTS failed_jobs (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
original_job_id INTEGER NOT NULL,
|
|
39
|
+
type TEXT NOT NULL,
|
|
40
|
+
data TEXT NOT NULL,
|
|
41
|
+
error TEXT,
|
|
42
|
+
retry_count INTEGER NOT NULL,
|
|
43
|
+
max_retries INTEGER NOT NULL,
|
|
44
|
+
created_at TEXT NOT NULL,
|
|
45
|
+
failed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
46
|
+
request_log TEXT,
|
|
47
|
+
response_log TEXT
|
|
48
|
+
)`;
|
|
49
|
+
const JOB_BATCHES_TABLE = `
|
|
50
|
+
CREATE TABLE IF NOT EXISTS job_batches (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
name TEXT NOT NULL,
|
|
53
|
+
total_jobs INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
pending_jobs INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
failed_jobs INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
failed_job_ids TEXT NOT NULL DEFAULT '[]',
|
|
57
|
+
options TEXT,
|
|
58
|
+
cancelled_at TEXT,
|
|
59
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
60
|
+
finished_at TEXT
|
|
61
|
+
)`;
|
|
62
|
+
const SCHEDULES_TABLE = `
|
|
63
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
name TEXT NOT NULL UNIQUE,
|
|
66
|
+
type TEXT NOT NULL,
|
|
67
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
68
|
+
cron TEXT NOT NULL,
|
|
69
|
+
timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
70
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
71
|
+
overlap INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
73
|
+
last_run_at TEXT,
|
|
74
|
+
next_run_at TEXT,
|
|
75
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
76
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
77
|
+
)`;
|
|
78
|
+
const SCHEDULES_NEXT_RUN_INDEX = `
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_next_run
|
|
80
|
+
ON schedules (enabled, next_run_at)`;
|
|
81
|
+
export function applyPragmas(db) {
|
|
82
|
+
if (db.filename !== ':memory:' && db.filename !== '') {
|
|
83
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
84
|
+
db.run('PRAGMA busy_timeout = 5000');
|
|
85
|
+
}
|
|
86
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
87
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
88
|
+
db.run('PRAGMA cache_size = -4000');
|
|
89
|
+
db.run('PRAGMA temp_store = MEMORY');
|
|
90
|
+
db.run('PRAGMA mmap_size = 16777216'); // 16MB — jobs rows are small, no blobs
|
|
91
|
+
db.run('PRAGMA wal_autocheckpoint = 0'); // Litestream controls checkpointing
|
|
92
|
+
}
|
|
93
|
+
export function initializeSchema(db) {
|
|
94
|
+
db.run(JOB_BATCHES_TABLE);
|
|
95
|
+
db.run(JOBS_TABLE);
|
|
96
|
+
db.run(JOBS_POLL_INDEX);
|
|
97
|
+
db.run(JOBS_STATUS_INDEX);
|
|
98
|
+
db.run(JOB_DEPENDENCIES_TABLE);
|
|
99
|
+
db.run(JOB_DEPS_INDEX);
|
|
100
|
+
db.run(FAILED_JOBS_TABLE);
|
|
101
|
+
db.run(SCHEDULES_TABLE);
|
|
102
|
+
db.run(SCHEDULES_NEXT_RUN_INDEX);
|
|
103
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throw this from a job handler to skip all retries and move immediately to
|
|
3
|
+
* the dead-letter (failed_jobs) table. Use for permanent configuration errors
|
|
4
|
+
* like missing SSH keys, bad tokens, or invalid input that will never succeed
|
|
5
|
+
* no matter how many times the job runs.
|
|
6
|
+
*/
|
|
7
|
+
export declare class NonRetryableError extends Error {
|
|
8
|
+
readonly isNonRetryable = true;
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export type JobStatus = 'pending' | 'processing' | 'done' | 'failed' | 'blocked' | 'cancelled';
|
|
12
|
+
export type Job<T = unknown> = {
|
|
13
|
+
readonly id: number;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly data: T;
|
|
16
|
+
readonly status: JobStatus;
|
|
17
|
+
readonly priority: number;
|
|
18
|
+
readonly progress: number;
|
|
19
|
+
readonly maxRetries: number;
|
|
20
|
+
readonly retryCount: number;
|
|
21
|
+
readonly runAt: string;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly updatedAt: string;
|
|
24
|
+
readonly startedAt: string | null;
|
|
25
|
+
readonly completedAt: string | null;
|
|
26
|
+
readonly error: string | null;
|
|
27
|
+
readonly batchId: string | null;
|
|
28
|
+
readonly requestLog: string | null;
|
|
29
|
+
readonly responseLog: string | null;
|
|
30
|
+
};
|
|
31
|
+
export type FailedJob = {
|
|
32
|
+
readonly id: number;
|
|
33
|
+
readonly originalJobId: number;
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly data: unknown;
|
|
36
|
+
readonly error: string | null;
|
|
37
|
+
readonly retryCount: number;
|
|
38
|
+
readonly maxRetries: number;
|
|
39
|
+
readonly createdAt: string;
|
|
40
|
+
readonly failedAt: string;
|
|
41
|
+
readonly requestLog: string | null;
|
|
42
|
+
readonly responseLog: string | null;
|
|
43
|
+
};
|
|
44
|
+
export type AddJobOptions = {
|
|
45
|
+
priority?: number;
|
|
46
|
+
runAt?: Date;
|
|
47
|
+
maxRetries?: number;
|
|
48
|
+
dependsOn?: number[];
|
|
49
|
+
};
|
|
50
|
+
export type JobContext = {
|
|
51
|
+
reportProgress: (percent: number) => void;
|
|
52
|
+
signal: AbortSignal;
|
|
53
|
+
};
|
|
54
|
+
export type RateLimit = {
|
|
55
|
+
count: number;
|
|
56
|
+
windowMs: number;
|
|
57
|
+
};
|
|
58
|
+
export type WorkerOptions<T = unknown> = {
|
|
59
|
+
type: string;
|
|
60
|
+
handler: (job: Job<T>, ctx: JobContext) => Promise<void>;
|
|
61
|
+
pollIntervalMs?: number;
|
|
62
|
+
maxRate?: RateLimit;
|
|
63
|
+
onError?: (job: Job<T>, error: unknown) => void;
|
|
64
|
+
/** Hard wall-clock limit per job execution in ms. Job is marked failed on timeout. */
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
};
|
|
67
|
+
export type JobStats = {
|
|
68
|
+
pending: number;
|
|
69
|
+
blocked: number;
|
|
70
|
+
processing: number;
|
|
71
|
+
done: number;
|
|
72
|
+
failed: number;
|
|
73
|
+
cancelled: number;
|
|
74
|
+
dead: number;
|
|
75
|
+
};
|
|
76
|
+
export type PurgeOptions = {
|
|
77
|
+
status: 'done' | 'failed';
|
|
78
|
+
olderThanMs: number;
|
|
79
|
+
};
|
|
80
|
+
export type ListJobsOptions = {
|
|
81
|
+
status?: JobStatus;
|
|
82
|
+
type?: string;
|
|
83
|
+
limit?: number;
|
|
84
|
+
offset?: number;
|
|
85
|
+
};
|
|
86
|
+
export type PaginatedResult<T> = {
|
|
87
|
+
items: T[];
|
|
88
|
+
total: number;
|
|
89
|
+
};
|
|
90
|
+
export type JobMap = Record<string, unknown>;
|
|
91
|
+
export type JobRow = {
|
|
92
|
+
id: number;
|
|
93
|
+
type: string;
|
|
94
|
+
data: string;
|
|
95
|
+
status: string;
|
|
96
|
+
priority: number;
|
|
97
|
+
progress: number;
|
|
98
|
+
max_retries: number;
|
|
99
|
+
retry_count: number;
|
|
100
|
+
run_at: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
updated_at: string;
|
|
103
|
+
started_at: string | null;
|
|
104
|
+
completed_at: string | null;
|
|
105
|
+
error: string | null;
|
|
106
|
+
batch_id: string | null;
|
|
107
|
+
request_log: string | null;
|
|
108
|
+
response_log: string | null;
|
|
109
|
+
};
|
|
110
|
+
export type FailedJobRow = {
|
|
111
|
+
id: number;
|
|
112
|
+
original_job_id: number;
|
|
113
|
+
type: string;
|
|
114
|
+
data: string;
|
|
115
|
+
error: string | null;
|
|
116
|
+
retry_count: number;
|
|
117
|
+
max_retries: number;
|
|
118
|
+
created_at: string;
|
|
119
|
+
failed_at: string;
|
|
120
|
+
request_log: string | null;
|
|
121
|
+
response_log: string | null;
|
|
122
|
+
};
|
|
123
|
+
export type StatsRow = {
|
|
124
|
+
status: string;
|
|
125
|
+
count: number;
|
|
126
|
+
};
|
|
127
|
+
export type BatchOptions = {
|
|
128
|
+
thenType?: string;
|
|
129
|
+
thenData?: unknown;
|
|
130
|
+
finallyType?: string;
|
|
131
|
+
finallyData?: unknown;
|
|
132
|
+
};
|
|
133
|
+
export type JobBatch = {
|
|
134
|
+
readonly id: string;
|
|
135
|
+
readonly name: string;
|
|
136
|
+
readonly totalJobs: number;
|
|
137
|
+
readonly pendingJobs: number;
|
|
138
|
+
readonly failedJobs: number;
|
|
139
|
+
readonly failedJobIds: number[];
|
|
140
|
+
readonly options: BatchOptions | null;
|
|
141
|
+
readonly cancelledAt: string | null;
|
|
142
|
+
readonly createdAt: string;
|
|
143
|
+
readonly finishedAt: string | null;
|
|
144
|
+
};
|
|
145
|
+
export type JobBatchRow = {
|
|
146
|
+
id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
total_jobs: number;
|
|
149
|
+
pending_jobs: number;
|
|
150
|
+
failed_jobs: number;
|
|
151
|
+
failed_job_ids: string;
|
|
152
|
+
options: string | null;
|
|
153
|
+
cancelled_at: string | null;
|
|
154
|
+
created_at: string;
|
|
155
|
+
finished_at: string | null;
|
|
156
|
+
};
|
|
157
|
+
export type Schedule = {
|
|
158
|
+
readonly id: number;
|
|
159
|
+
readonly name: string;
|
|
160
|
+
readonly type: string;
|
|
161
|
+
readonly data: unknown;
|
|
162
|
+
readonly cron: string;
|
|
163
|
+
readonly timezone: string;
|
|
164
|
+
readonly enabled: boolean;
|
|
165
|
+
readonly overlap: boolean;
|
|
166
|
+
readonly maxRetries: number;
|
|
167
|
+
readonly lastRunAt: string | null;
|
|
168
|
+
readonly nextRunAt: string | null;
|
|
169
|
+
readonly createdAt: string;
|
|
170
|
+
readonly updatedAt: string;
|
|
171
|
+
};
|
|
172
|
+
export type ScheduleRow = {
|
|
173
|
+
id: number;
|
|
174
|
+
name: string;
|
|
175
|
+
type: string;
|
|
176
|
+
data: string;
|
|
177
|
+
cron: string;
|
|
178
|
+
timezone: string;
|
|
179
|
+
enabled: number;
|
|
180
|
+
overlap: number;
|
|
181
|
+
max_retries: number;
|
|
182
|
+
last_run_at: string | null;
|
|
183
|
+
next_run_at: string | null;
|
|
184
|
+
created_at: string;
|
|
185
|
+
updated_at: string;
|
|
186
|
+
};
|
|
187
|
+
export type AddScheduleOptions = {
|
|
188
|
+
data?: unknown;
|
|
189
|
+
timezone?: string;
|
|
190
|
+
overlap?: boolean;
|
|
191
|
+
maxRetries?: number;
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// packages/jobs/src/types.ts
|
|
2
|
+
// All type definitions for the SQLite background job queue
|
|
3
|
+
/**
|
|
4
|
+
* Throw this from a job handler to skip all retries and move immediately to
|
|
5
|
+
* the dead-letter (failed_jobs) table. Use for permanent configuration errors
|
|
6
|
+
* like missing SSH keys, bad tokens, or invalid input that will never succeed
|
|
7
|
+
* no matter how many times the job runs.
|
|
8
|
+
*/
|
|
9
|
+
export class NonRetryableError extends Error {
|
|
10
|
+
isNonRetryable = true;
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'NonRetryableError';
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
|
package/dist/utils.js
ADDED
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JobQueue } from './queue';
|
|
2
|
+
import type { JobMap, WorkerOptions } from './types';
|
|
3
|
+
export declare class JobWorker<TMap extends JobMap = Record<string, unknown>, K extends string & keyof TMap = string & keyof TMap> {
|
|
4
|
+
private queue;
|
|
5
|
+
private options;
|
|
6
|
+
private rateLimiter;
|
|
7
|
+
private abortController;
|
|
8
|
+
private timer;
|
|
9
|
+
private running;
|
|
10
|
+
private processing;
|
|
11
|
+
private stopResolve;
|
|
12
|
+
constructor(queue: JobQueue<TMap>, options: WorkerOptions<TMap[K]> & {
|
|
13
|
+
type: K;
|
|
14
|
+
});
|
|
15
|
+
get isRunning(): boolean;
|
|
16
|
+
start(): void;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
private scheduleNext;
|
|
19
|
+
private poll;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,KAAK,EAAmB,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGtE,qBAAa,SAAS,CACpB,IAAI,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,IAAI;IAEnD,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAA6B;gBAG9C,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EACrB,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE;IAY/C,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,KAAK,IAAI,IAAI;IAOP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B,OAAO,CAAC,YAAY;YAMN,IAAI;CAqEnB"}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SlidingWindowRateLimiter } from './rate-limiter';
|
|
2
|
+
import { NonRetryableError } from './types';
|
|
3
|
+
export class JobWorker {
|
|
4
|
+
queue;
|
|
5
|
+
options;
|
|
6
|
+
rateLimiter;
|
|
7
|
+
abortController = null;
|
|
8
|
+
timer = null;
|
|
9
|
+
running = false;
|
|
10
|
+
processing = false;
|
|
11
|
+
stopResolve = null;
|
|
12
|
+
constructor(queue, options) {
|
|
13
|
+
this.queue = queue;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.rateLimiter = options.maxRate
|
|
16
|
+
? new SlidingWindowRateLimiter(options.maxRate.count, options.maxRate.windowMs)
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
get isRunning() {
|
|
20
|
+
return this.running;
|
|
21
|
+
}
|
|
22
|
+
start() {
|
|
23
|
+
if (this.running)
|
|
24
|
+
return;
|
|
25
|
+
this.running = true;
|
|
26
|
+
this.abortController = new AbortController();
|
|
27
|
+
this.scheduleNext();
|
|
28
|
+
}
|
|
29
|
+
async stop() {
|
|
30
|
+
if (!this.running)
|
|
31
|
+
return;
|
|
32
|
+
this.running = false;
|
|
33
|
+
if (this.timer) {
|
|
34
|
+
clearTimeout(this.timer);
|
|
35
|
+
this.timer = null;
|
|
36
|
+
}
|
|
37
|
+
this.abortController?.abort();
|
|
38
|
+
if (this.processing) {
|
|
39
|
+
return new Promise(resolve => {
|
|
40
|
+
this.stopResolve = resolve;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
scheduleNext() {
|
|
45
|
+
if (!this.running)
|
|
46
|
+
return;
|
|
47
|
+
const interval = this.options.pollIntervalMs ?? 1000;
|
|
48
|
+
this.timer = setTimeout(() => this.poll(), interval);
|
|
49
|
+
}
|
|
50
|
+
async poll() {
|
|
51
|
+
if (!this.running)
|
|
52
|
+
return;
|
|
53
|
+
if (this.rateLimiter && !this.rateLimiter.canProceed()) {
|
|
54
|
+
this.scheduleNext();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const job = this.queue.pollAndClaim(this.options.type);
|
|
58
|
+
if (!job) {
|
|
59
|
+
this.scheduleNext();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.processing = true;
|
|
63
|
+
try {
|
|
64
|
+
const ctx = {
|
|
65
|
+
reportProgress: (percent) => {
|
|
66
|
+
this.queue.updateProgress(job.id, percent);
|
|
67
|
+
},
|
|
68
|
+
signal: this.abortController.signal
|
|
69
|
+
};
|
|
70
|
+
this.rateLimiter?.record();
|
|
71
|
+
const handlerPromise = this.options.handler(job, ctx);
|
|
72
|
+
if (this.options.timeoutMs) {
|
|
73
|
+
const timeoutMs = this.options.timeoutMs;
|
|
74
|
+
await Promise.race([
|
|
75
|
+
handlerPromise,
|
|
76
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Job timed out after ${timeoutMs}ms`)), timeoutMs))
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await handlerPromise;
|
|
81
|
+
}
|
|
82
|
+
this.queue.markJobDone(job.id);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
// Check both instanceof (same bundle) and the isNonRetryable property
|
|
87
|
+
// (duck-type fallback in case module deduplication fails across Vite chunks)
|
|
88
|
+
const isNonRetryable = error instanceof NonRetryableError ||
|
|
89
|
+
(typeof error === 'object' &&
|
|
90
|
+
error !== null &&
|
|
91
|
+
error.isNonRetryable === true);
|
|
92
|
+
if (isNonRetryable) {
|
|
93
|
+
this.queue.markJobDead(job.id, message);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.queue.markJobFailed(job.id, message);
|
|
97
|
+
}
|
|
98
|
+
this.options.onError?.(job, error);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this.processing = false;
|
|
102
|
+
if (this.stopResolve) {
|
|
103
|
+
this.stopResolve();
|
|
104
|
+
this.stopResolve = null;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.scheduleNext();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|