@happyvertical/jobs 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/dist/adapters/bull.d.ts +103 -0
- package/dist/adapters/bull.d.ts.map +1 -0
- package/dist/adapters/bull.js +349 -0
- package/dist/adapters/bull.js.map +1 -0
- package/dist/adapters/bullmq.d.ts +85 -0
- package/dist/adapters/bullmq.d.ts.map +1 -0
- package/dist/adapters/bullmq.js +391 -0
- package/dist/adapters/bullmq.js.map +1 -0
- package/dist/adapters/cloud-tasks.d.ts +110 -0
- package/dist/adapters/cloud-tasks.d.ts.map +1 -0
- package/dist/adapters/cloud-tasks.js +336 -0
- package/dist/adapters/cloud-tasks.js.map +1 -0
- package/dist/adapters/postgres.d.ts +55 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +437 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +44 -0
- package/dist/adapters/sqlite.d.ts.map +1 -0
- package/dist/adapters/sqlite.js +323 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/adapters/sqs.d.ts +112 -0
- package/dist/adapters/sqs.d.ts.map +1 -0
- package/dist/adapters/sqs.js +411 -0
- package/dist/adapters/sqs.js.map +1 -0
- package/dist/base-store.d.ts +69 -0
- package/dist/base-store.d.ts.map +1 -0
- package/dist/chunks/base-store-DlNksWvQ.js +324 -0
- package/dist/chunks/base-store-DlNksWvQ.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/dist/retry.d.ts +84 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/worker.d.ts +74 -0
- package/dist/worker.d.ts.map +1 -0
- package/metadata.json +34 -0
- package/package.json +114 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createId } from "@happyvertical/utils";
|
|
2
|
+
class ExponentialBackoffStrategy {
|
|
3
|
+
initialDelay;
|
|
4
|
+
maxDelay;
|
|
5
|
+
multiplier;
|
|
6
|
+
jitter;
|
|
7
|
+
maxAttempts;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.initialDelay = options.initialDelay ?? 1e3;
|
|
10
|
+
this.maxDelay = options.maxDelay ?? 3e5;
|
|
11
|
+
this.multiplier = options.multiplier ?? 2;
|
|
12
|
+
this.jitter = options.jitter ?? true;
|
|
13
|
+
this.maxAttempts = options.maxAttempts ?? null;
|
|
14
|
+
}
|
|
15
|
+
shouldRetry(attempt, _error) {
|
|
16
|
+
if (this.maxAttempts !== null && attempt >= this.maxAttempts) {
|
|
17
|
+
return { shouldRetry: false, delay: 0 };
|
|
18
|
+
}
|
|
19
|
+
let delay = this.initialDelay * this.multiplier ** (attempt - 1);
|
|
20
|
+
delay = Math.min(delay, this.maxDelay);
|
|
21
|
+
if (this.jitter) {
|
|
22
|
+
const jitterRange = delay * 0.25;
|
|
23
|
+
delay = delay - jitterRange + Math.random() * jitterRange * 2;
|
|
24
|
+
}
|
|
25
|
+
return { shouldRetry: true, delay: Math.round(delay) };
|
|
26
|
+
}
|
|
27
|
+
toConfig() {
|
|
28
|
+
return {
|
|
29
|
+
type: "exponential",
|
|
30
|
+
config: {
|
|
31
|
+
initialDelay: this.initialDelay,
|
|
32
|
+
maxDelay: this.maxDelay,
|
|
33
|
+
multiplier: this.multiplier,
|
|
34
|
+
jitter: this.jitter,
|
|
35
|
+
maxAttempts: this.maxAttempts
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class LinearBackoffStrategy {
|
|
41
|
+
delay;
|
|
42
|
+
maxAttempts;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.delay = options.delay ?? 5e3;
|
|
45
|
+
this.maxAttempts = options.maxAttempts ?? null;
|
|
46
|
+
}
|
|
47
|
+
shouldRetry(attempt, _error) {
|
|
48
|
+
if (this.maxAttempts !== null && attempt >= this.maxAttempts) {
|
|
49
|
+
return { shouldRetry: false, delay: 0 };
|
|
50
|
+
}
|
|
51
|
+
return { shouldRetry: true, delay: this.delay };
|
|
52
|
+
}
|
|
53
|
+
toConfig() {
|
|
54
|
+
return {
|
|
55
|
+
type: "linear",
|
|
56
|
+
config: {
|
|
57
|
+
delay: this.delay,
|
|
58
|
+
maxAttempts: this.maxAttempts
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class CustomRetryStrategy {
|
|
64
|
+
fn;
|
|
65
|
+
fnString;
|
|
66
|
+
constructor(fn) {
|
|
67
|
+
this.fn = fn;
|
|
68
|
+
this.fnString = fn.toString();
|
|
69
|
+
}
|
|
70
|
+
shouldRetry(attempt, error) {
|
|
71
|
+
return this.fn(attempt, error);
|
|
72
|
+
}
|
|
73
|
+
toConfig() {
|
|
74
|
+
return {
|
|
75
|
+
type: "custom",
|
|
76
|
+
config: {
|
|
77
|
+
fn: this.fnString
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
class NoRetryStrategy {
|
|
83
|
+
shouldRetry(_attempt, _error) {
|
|
84
|
+
return { shouldRetry: false, delay: 0 };
|
|
85
|
+
}
|
|
86
|
+
toConfig() {
|
|
87
|
+
return {
|
|
88
|
+
type: "linear",
|
|
89
|
+
config: { maxAttempts: 1 }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function exponential(options) {
|
|
94
|
+
return new ExponentialBackoffStrategy(options);
|
|
95
|
+
}
|
|
96
|
+
function linear(options) {
|
|
97
|
+
return new LinearBackoffStrategy(options);
|
|
98
|
+
}
|
|
99
|
+
function custom(fn) {
|
|
100
|
+
return new CustomRetryStrategy(fn);
|
|
101
|
+
}
|
|
102
|
+
function noRetry() {
|
|
103
|
+
return new NoRetryStrategy();
|
|
104
|
+
}
|
|
105
|
+
function fromConfig(config) {
|
|
106
|
+
switch (config.type) {
|
|
107
|
+
case "exponential":
|
|
108
|
+
return new ExponentialBackoffStrategy(
|
|
109
|
+
config.config
|
|
110
|
+
);
|
|
111
|
+
case "linear":
|
|
112
|
+
return new LinearBackoffStrategy(config.config);
|
|
113
|
+
case "custom":
|
|
114
|
+
console.warn(
|
|
115
|
+
"Custom retry strategy cannot be reconstructed from config, using exponential"
|
|
116
|
+
);
|
|
117
|
+
return new ExponentialBackoffStrategy();
|
|
118
|
+
default:
|
|
119
|
+
return new ExponentialBackoffStrategy();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const DEFAULT_RETRY_STRATEGY = exponential({
|
|
123
|
+
initialDelay: 1e3,
|
|
124
|
+
maxDelay: 3e5,
|
|
125
|
+
multiplier: 2,
|
|
126
|
+
jitter: true
|
|
127
|
+
});
|
|
128
|
+
const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
129
|
+
function validateTableName(tableName) {
|
|
130
|
+
if (!tableName || tableName.length === 0) {
|
|
131
|
+
throw new Error("Table name cannot be empty");
|
|
132
|
+
}
|
|
133
|
+
if (tableName.length > 128) {
|
|
134
|
+
throw new Error("Table name cannot exceed 128 characters");
|
|
135
|
+
}
|
|
136
|
+
if (!TABLE_NAME_PATTERN.test(tableName)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Invalid table name "${tableName}": must contain only alphanumeric characters and underscores, and must start with a letter or underscore`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return tableName;
|
|
142
|
+
}
|
|
143
|
+
function priorityToNumber(priority) {
|
|
144
|
+
if (typeof priority === "number") return priority;
|
|
145
|
+
switch (priority) {
|
|
146
|
+
case "critical":
|
|
147
|
+
return 100;
|
|
148
|
+
case "high":
|
|
149
|
+
return 75;
|
|
150
|
+
case "normal":
|
|
151
|
+
return 50;
|
|
152
|
+
case "low":
|
|
153
|
+
return 25;
|
|
154
|
+
default:
|
|
155
|
+
return 50;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
class BaseJobStore {
|
|
159
|
+
listeners = /* @__PURE__ */ new Set();
|
|
160
|
+
initialized = false;
|
|
161
|
+
/**
|
|
162
|
+
* Create a new job record
|
|
163
|
+
*/
|
|
164
|
+
createJobRecord(options) {
|
|
165
|
+
const now = /* @__PURE__ */ new Date();
|
|
166
|
+
const retryConfig = options.retryStrategy && "toConfig" in options.retryStrategy ? options.retryStrategy.toConfig() : options.retryStrategy ?? DEFAULT_RETRY_STRATEGY.toConfig();
|
|
167
|
+
return {
|
|
168
|
+
id: createId(),
|
|
169
|
+
queue: options.queue ?? "default",
|
|
170
|
+
payload: options.payload,
|
|
171
|
+
status: "pending",
|
|
172
|
+
priority: priorityToNumber(options.priority),
|
|
173
|
+
attempts: 0,
|
|
174
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
175
|
+
runAt: options.runAt ?? now,
|
|
176
|
+
startedAt: null,
|
|
177
|
+
completedAt: null,
|
|
178
|
+
timeout: options.timeout ?? 3e5,
|
|
179
|
+
timeoutBehavior: options.timeoutBehavior ?? "fail",
|
|
180
|
+
lastError: null,
|
|
181
|
+
resultPointer: null,
|
|
182
|
+
retryStrategy: retryConfig,
|
|
183
|
+
workerId: null,
|
|
184
|
+
workerHeartbeat: null,
|
|
185
|
+
createdAt: now,
|
|
186
|
+
updatedAt: now
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Emit a job event to all listeners
|
|
191
|
+
*/
|
|
192
|
+
async emitEvent(type, job, extra) {
|
|
193
|
+
const event = {
|
|
194
|
+
type,
|
|
195
|
+
job,
|
|
196
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
197
|
+
...extra
|
|
198
|
+
};
|
|
199
|
+
const promises = Array.from(this.listeners).map(
|
|
200
|
+
(listener) => Promise.resolve(listener(event)).catch((err) => {
|
|
201
|
+
console.error("Job event listener error:", err);
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
await Promise.allSettled(promises);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Subscribe to job events
|
|
208
|
+
*/
|
|
209
|
+
subscribe(listener) {
|
|
210
|
+
this.listeners.add(listener);
|
|
211
|
+
return () => {
|
|
212
|
+
this.listeners.delete(listener);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Build WHERE clause for filters
|
|
217
|
+
*/
|
|
218
|
+
buildFilterWhere(filter) {
|
|
219
|
+
const conditions = [];
|
|
220
|
+
const params = [];
|
|
221
|
+
if (filter.queue) {
|
|
222
|
+
conditions.push("queue = ?");
|
|
223
|
+
params.push(filter.queue);
|
|
224
|
+
}
|
|
225
|
+
if (filter.status) {
|
|
226
|
+
if (Array.isArray(filter.status)) {
|
|
227
|
+
const placeholders = filter.status.map(() => "?").join(", ");
|
|
228
|
+
conditions.push(`status IN (${placeholders})`);
|
|
229
|
+
params.push(...filter.status);
|
|
230
|
+
} else {
|
|
231
|
+
conditions.push("status = ?");
|
|
232
|
+
params.push(filter.status);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (filter.objectType) {
|
|
236
|
+
conditions.push("json_extract(payload, '$.objectType') = ?");
|
|
237
|
+
params.push(filter.objectType);
|
|
238
|
+
}
|
|
239
|
+
if (filter.method) {
|
|
240
|
+
conditions.push("json_extract(payload, '$.method') = ?");
|
|
241
|
+
params.push(filter.method);
|
|
242
|
+
}
|
|
243
|
+
if (filter.createdAfter) {
|
|
244
|
+
conditions.push("created_at > ?");
|
|
245
|
+
params.push(filter.createdAfter.toISOString());
|
|
246
|
+
}
|
|
247
|
+
if (filter.createdBefore) {
|
|
248
|
+
conditions.push("created_at < ?");
|
|
249
|
+
params.push(filter.createdBefore.toISOString());
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
where: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
253
|
+
params
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Build ORDER BY clause for filters
|
|
258
|
+
*/
|
|
259
|
+
buildOrderBy(filter) {
|
|
260
|
+
const field = filter.orderBy ?? "createdAt";
|
|
261
|
+
const dir = filter.orderDir ?? "desc";
|
|
262
|
+
const fieldMap = {
|
|
263
|
+
createdAt: "created_at",
|
|
264
|
+
runAt: "run_at",
|
|
265
|
+
priority: "priority",
|
|
266
|
+
attempts: "attempts"
|
|
267
|
+
};
|
|
268
|
+
return `ORDER BY ${fieldMap[field] ?? "created_at"} ${dir.toUpperCase()}`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Build LIMIT/OFFSET clause
|
|
272
|
+
*/
|
|
273
|
+
buildLimitOffset(filter) {
|
|
274
|
+
const params = [];
|
|
275
|
+
let clause = "";
|
|
276
|
+
if (filter.limit) {
|
|
277
|
+
clause = "LIMIT ?";
|
|
278
|
+
params.push(filter.limit);
|
|
279
|
+
if (filter.offset) {
|
|
280
|
+
clause += " OFFSET ?";
|
|
281
|
+
params.push(filter.offset);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { clause, params };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Parse a job row from the database
|
|
288
|
+
*/
|
|
289
|
+
parseJobRow(row) {
|
|
290
|
+
return {
|
|
291
|
+
id: row.id,
|
|
292
|
+
queue: row.queue,
|
|
293
|
+
payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
|
|
294
|
+
status: row.status,
|
|
295
|
+
priority: row.priority,
|
|
296
|
+
attempts: row.attempts,
|
|
297
|
+
maxAttempts: row.max_attempts,
|
|
298
|
+
runAt: new Date(row.run_at),
|
|
299
|
+
startedAt: row.started_at ? new Date(row.started_at) : null,
|
|
300
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
301
|
+
timeout: row.timeout,
|
|
302
|
+
timeoutBehavior: row.timeout_behavior,
|
|
303
|
+
lastError: row.last_error,
|
|
304
|
+
resultPointer: row.result_pointer,
|
|
305
|
+
retryStrategy: typeof row.retry_strategy === "string" ? JSON.parse(row.retry_strategy) : row.retry_strategy,
|
|
306
|
+
workerId: row.worker_id,
|
|
307
|
+
workerHeartbeat: row.worker_heartbeat ? new Date(row.worker_heartbeat) : null,
|
|
308
|
+
createdAt: new Date(row.created_at),
|
|
309
|
+
updatedAt: new Date(row.updated_at)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export {
|
|
314
|
+
BaseJobStore as B,
|
|
315
|
+
DEFAULT_RETRY_STRATEGY as D,
|
|
316
|
+
custom as c,
|
|
317
|
+
exponential as e,
|
|
318
|
+
fromConfig as f,
|
|
319
|
+
linear as l,
|
|
320
|
+
noRetry as n,
|
|
321
|
+
priorityToNumber as p,
|
|
322
|
+
validateTableName as v
|
|
323
|
+
};
|
|
324
|
+
//# sourceMappingURL=base-store-DlNksWvQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-store-DlNksWvQ.js","sources":["../../src/retry.ts","../../src/base-store.ts"],"sourcesContent":["import type {\n RetryDecision,\n RetryStrategy,\n RetryStrategyConfig,\n} from './types.js';\n\n/**\n * Options for exponential backoff retry strategy\n */\nexport interface ExponentialBackoffOptions {\n /** Initial delay in milliseconds (default: 1000) */\n initialDelay?: number;\n /** Maximum delay in milliseconds (default: 300000 = 5 minutes) */\n maxDelay?: number;\n /** Multiplier for each attempt (default: 2) */\n multiplier?: number;\n /** Add random jitter to prevent thundering herd (default: true) */\n jitter?: boolean;\n /** Maximum attempts (optional, can also be set on job) */\n maxAttempts?: number;\n}\n\n/**\n * Exponential backoff retry strategy\n *\n * Delay increases exponentially: initialDelay * multiplier^(attempt-1)\n * With optional jitter to prevent thundering herd problem.\n */\nclass ExponentialBackoffStrategy implements RetryStrategy {\n private readonly initialDelay: number;\n private readonly maxDelay: number;\n private readonly multiplier: number;\n private readonly jitter: boolean;\n private readonly maxAttempts: number | null;\n\n constructor(options: ExponentialBackoffOptions = {}) {\n this.initialDelay = options.initialDelay ?? 1000;\n this.maxDelay = options.maxDelay ?? 300000;\n this.multiplier = options.multiplier ?? 2;\n this.jitter = options.jitter ?? true;\n this.maxAttempts = options.maxAttempts ?? null;\n }\n\n shouldRetry(attempt: number, _error: Error): RetryDecision {\n // Check max attempts if configured\n if (this.maxAttempts !== null && attempt >= this.maxAttempts) {\n return { shouldRetry: false, delay: 0 };\n }\n\n // Calculate base delay: initialDelay * multiplier^(attempt-1)\n let delay = this.initialDelay * this.multiplier ** (attempt - 1);\n\n // Cap at maxDelay\n delay = Math.min(delay, this.maxDelay);\n\n // Add jitter (±25% randomization)\n if (this.jitter) {\n const jitterRange = delay * 0.25;\n delay = delay - jitterRange + Math.random() * jitterRange * 2;\n }\n\n return { shouldRetry: true, delay: Math.round(delay) };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'exponential',\n config: {\n initialDelay: this.initialDelay,\n maxDelay: this.maxDelay,\n multiplier: this.multiplier,\n jitter: this.jitter,\n maxAttempts: this.maxAttempts,\n },\n };\n }\n}\n\n/**\n * Options for linear retry strategy\n */\nexport interface LinearBackoffOptions {\n /** Fixed delay between retries in milliseconds (default: 5000) */\n delay?: number;\n /** Maximum attempts (optional) */\n maxAttempts?: number;\n}\n\n/**\n * Linear retry strategy\n *\n * Uses a fixed delay between all retry attempts.\n */\nclass LinearBackoffStrategy implements RetryStrategy {\n private readonly delay: number;\n private readonly maxAttempts: number | null;\n\n constructor(options: LinearBackoffOptions = {}) {\n this.delay = options.delay ?? 5000;\n this.maxAttempts = options.maxAttempts ?? null;\n }\n\n shouldRetry(attempt: number, _error: Error): RetryDecision {\n if (this.maxAttempts !== null && attempt >= this.maxAttempts) {\n return { shouldRetry: false, delay: 0 };\n }\n\n return { shouldRetry: true, delay: this.delay };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'linear',\n config: {\n delay: this.delay,\n maxAttempts: this.maxAttempts,\n },\n };\n }\n}\n\n/**\n * Custom retry decision function\n */\nexport type CustomRetryFn = (attempt: number, error: Error) => RetryDecision;\n\n/**\n * Custom retry strategy\n *\n * Allows full control over retry logic via a custom function.\n */\nclass CustomRetryStrategy implements RetryStrategy {\n private readonly fn: CustomRetryFn;\n private readonly fnString: string;\n\n constructor(fn: CustomRetryFn) {\n this.fn = fn;\n // Store string representation for serialization\n this.fnString = fn.toString();\n }\n\n shouldRetry(attempt: number, error: Error): RetryDecision {\n return this.fn(attempt, error);\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'custom',\n config: {\n fn: this.fnString,\n },\n };\n }\n}\n\n/**\n * No retry strategy - never retry\n */\nclass NoRetryStrategy implements RetryStrategy {\n shouldRetry(_attempt: number, _error: Error): RetryDecision {\n return { shouldRetry: false, delay: 0 };\n }\n\n toConfig(): RetryStrategyConfig {\n return {\n type: 'linear',\n config: { maxAttempts: 1 },\n };\n }\n}\n\n// Factory functions\n\n/**\n * Create an exponential backoff retry strategy\n *\n * @example\n * ```typescript\n * const strategy = exponential({\n * initialDelay: 1000,\n * maxDelay: 300000,\n * multiplier: 2,\n * jitter: true,\n * });\n * ```\n */\nexport function exponential(\n options?: ExponentialBackoffOptions,\n): RetryStrategy {\n return new ExponentialBackoffStrategy(options);\n}\n\n/**\n * Create a linear retry strategy with fixed delay\n *\n * @example\n * ```typescript\n * const strategy = linear({ delay: 5000 });\n * ```\n */\nexport function linear(options?: LinearBackoffOptions): RetryStrategy {\n return new LinearBackoffStrategy(options);\n}\n\n/**\n * Create a custom retry strategy\n *\n * @example\n * ```typescript\n * const strategy = custom((attempt, error) => {\n * if (error.message.includes('RATE_LIMITED')) {\n * return { shouldRetry: true, delay: 60000 };\n * }\n * return { shouldRetry: attempt < 3, delay: attempt * 1000 };\n * });\n * ```\n */\nexport function custom(fn: CustomRetryFn): RetryStrategy {\n return new CustomRetryStrategy(fn);\n}\n\n/**\n * Create a no-retry strategy\n *\n * @example\n * ```typescript\n * const strategy = noRetry();\n * ```\n */\nexport function noRetry(): RetryStrategy {\n return new NoRetryStrategy();\n}\n\n/**\n * Reconstruct a retry strategy from its config\n */\nexport function fromConfig(config: RetryStrategyConfig): RetryStrategy {\n switch (config.type) {\n case 'exponential':\n return new ExponentialBackoffStrategy(\n config.config as ExponentialBackoffOptions,\n );\n case 'linear':\n return new LinearBackoffStrategy(config.config as LinearBackoffOptions);\n case 'custom':\n // For custom strategies loaded from config, we can't reconstruct the function\n // So we fall back to exponential\n console.warn(\n 'Custom retry strategy cannot be reconstructed from config, using exponential',\n );\n return new ExponentialBackoffStrategy();\n default:\n return new ExponentialBackoffStrategy();\n }\n}\n\n/**\n * Default retry strategy\n */\nexport const DEFAULT_RETRY_STRATEGY = exponential({\n initialDelay: 1000,\n maxDelay: 300000,\n multiplier: 2,\n jitter: true,\n});\n","import { createId } from '@happyvertical/utils';\nimport { DEFAULT_RETRY_STRATEGY } from './retry.js';\nimport type {\n CleanupOptions,\n Job,\n JobCreateOptions,\n JobEvent,\n JobEventListener,\n JobEventType,\n JobFilter,\n JobPriority,\n JobStore,\n QueueStats,\n RetryStrategyConfig,\n} from './types.js';\n\n/**\n * Valid table name pattern: alphanumeric and underscores, must start with letter or underscore\n */\nconst TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validate table name to prevent SQL injection\n * @throws Error if table name contains invalid characters\n */\nexport function validateTableName(tableName: string): string {\n if (!tableName || tableName.length === 0) {\n throw new Error('Table name cannot be empty');\n }\n if (tableName.length > 128) {\n throw new Error('Table name cannot exceed 128 characters');\n }\n if (!TABLE_NAME_PATTERN.test(tableName)) {\n throw new Error(\n `Invalid table name \"${tableName}\": must contain only alphanumeric characters and underscores, and must start with a letter or underscore`,\n );\n }\n return tableName;\n}\n\n/**\n * Convert priority string to number\n */\nexport function priorityToNumber(\n priority: JobPriority | number | undefined,\n): number {\n if (typeof priority === 'number') return priority;\n switch (priority) {\n case 'critical':\n return 100;\n case 'high':\n return 75;\n case 'normal':\n return 50;\n case 'low':\n return 25;\n default:\n return 50;\n }\n}\n\n/**\n * Base job store with common functionality\n */\nexport abstract class BaseJobStore implements JobStore {\n protected listeners: Set<JobEventListener> = new Set();\n protected initialized = false;\n\n /**\n * Initialize the store - must be implemented by subclasses\n */\n abstract initialize(): Promise<void>;\n\n /**\n * Create a new job record\n */\n protected createJobRecord(options: JobCreateOptions): Job {\n const now = new Date();\n const retryConfig: RetryStrategyConfig =\n options.retryStrategy && 'toConfig' in options.retryStrategy\n ? options.retryStrategy.toConfig()\n : ((options.retryStrategy as RetryStrategyConfig) ??\n DEFAULT_RETRY_STRATEGY.toConfig());\n\n return {\n id: createId(),\n queue: options.queue ?? 'default',\n payload: options.payload,\n status: 'pending',\n priority: priorityToNumber(options.priority),\n attempts: 0,\n maxAttempts: options.maxAttempts ?? 3,\n runAt: options.runAt ?? now,\n startedAt: null,\n completedAt: null,\n timeout: options.timeout ?? 300000,\n timeoutBehavior: options.timeoutBehavior ?? 'fail',\n lastError: null,\n resultPointer: null,\n retryStrategy: retryConfig,\n workerId: null,\n workerHeartbeat: null,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * Emit a job event to all listeners\n */\n protected async emitEvent(\n type: JobEventType,\n job: Job,\n extra?: { error?: string; resultPointer?: string },\n ): Promise<void> {\n const event: JobEvent = {\n type,\n job,\n timestamp: new Date(),\n ...extra,\n };\n\n const promises = Array.from(this.listeners).map((listener) =>\n Promise.resolve(listener(event)).catch((err) => {\n console.error('Job event listener error:', err);\n }),\n );\n\n await Promise.allSettled(promises);\n }\n\n /**\n * Subscribe to job events\n */\n subscribe(listener: JobEventListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Build WHERE clause for filters\n */\n protected buildFilterWhere(filter: JobFilter): {\n where: string;\n params: unknown[];\n } {\n const conditions: string[] = [];\n const params: unknown[] = [];\n\n if (filter.queue) {\n conditions.push('queue = ?');\n params.push(filter.queue);\n }\n\n if (filter.status) {\n if (Array.isArray(filter.status)) {\n const placeholders = filter.status.map(() => '?').join(', ');\n conditions.push(`status IN (${placeholders})`);\n params.push(...filter.status);\n } else {\n conditions.push('status = ?');\n params.push(filter.status);\n }\n }\n\n if (filter.objectType) {\n conditions.push(\"json_extract(payload, '$.objectType') = ?\");\n params.push(filter.objectType);\n }\n\n if (filter.method) {\n conditions.push(\"json_extract(payload, '$.method') = ?\");\n params.push(filter.method);\n }\n\n if (filter.createdAfter) {\n conditions.push('created_at > ?');\n params.push(filter.createdAfter.toISOString());\n }\n\n if (filter.createdBefore) {\n conditions.push('created_at < ?');\n params.push(filter.createdBefore.toISOString());\n }\n\n return {\n where: conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '',\n params,\n };\n }\n\n /**\n * Build ORDER BY clause for filters\n */\n protected buildOrderBy(filter: JobFilter): string {\n const field = filter.orderBy ?? 'createdAt';\n const dir = filter.orderDir ?? 'desc';\n\n const fieldMap: Record<string, string> = {\n createdAt: 'created_at',\n runAt: 'run_at',\n priority: 'priority',\n attempts: 'attempts',\n };\n\n return `ORDER BY ${fieldMap[field] ?? 'created_at'} ${dir.toUpperCase()}`;\n }\n\n /**\n * Build LIMIT/OFFSET clause\n */\n protected buildLimitOffset(filter: JobFilter): {\n clause: string;\n params: unknown[];\n } {\n const params: unknown[] = [];\n let clause = '';\n\n if (filter.limit) {\n clause = 'LIMIT ?';\n params.push(filter.limit);\n\n if (filter.offset) {\n clause += ' OFFSET ?';\n params.push(filter.offset);\n }\n }\n\n return { clause, params };\n }\n\n /**\n * Parse a job row from the database\n */\n protected parseJobRow(row: Record<string, unknown>): Job {\n return {\n id: row.id as string,\n queue: row.queue as string,\n payload:\n typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload,\n status: row.status as Job['status'],\n priority: row.priority as number,\n attempts: row.attempts as number,\n maxAttempts: row.max_attempts as number,\n runAt: new Date(row.run_at as string),\n startedAt: row.started_at ? new Date(row.started_at as string) : null,\n completedAt: row.completed_at\n ? new Date(row.completed_at as string)\n : null,\n timeout: row.timeout as number,\n timeoutBehavior: row.timeout_behavior as Job['timeoutBehavior'],\n lastError: row.last_error as string | null,\n resultPointer: row.result_pointer as string | null,\n retryStrategy:\n typeof row.retry_strategy === 'string'\n ? JSON.parse(row.retry_strategy)\n : row.retry_strategy,\n workerId: row.worker_id as string | null,\n workerHeartbeat: row.worker_heartbeat\n ? new Date(row.worker_heartbeat as string)\n : null,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string),\n };\n }\n\n // Abstract methods that must be implemented by subclasses\n abstract enqueue(options: JobCreateOptions): Promise<Job>;\n abstract dequeue(\n queues: string[],\n limit: number,\n workerId: string,\n ): Promise<Job[]>;\n abstract update(id: string, updates: Partial<Job>): Promise<Job>;\n abstract get(id: string): Promise<Job | null>;\n abstract list(filter: JobFilter): Promise<Job[]>;\n abstract cancel(id: string): Promise<void>;\n abstract cleanup(options: CleanupOptions): Promise<number>;\n abstract heartbeat(jobId: string, workerId: string): Promise<void>;\n abstract stats(queue?: string): Promise<QueueStats>;\n abstract close(): Promise<void>;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-context.d.ts","sourceRoot":"","sources":["../../src/cli/claude-context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const Dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkgRoot = join(Dirname, "../..");
|
|
7
|
+
const targetDir = join(process.cwd(), ".claude");
|
|
8
|
+
if (!existsSync(targetDir)) {
|
|
9
|
+
mkdirSync(targetDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
const pkgName = "jobs";
|
|
12
|
+
const agentMdSrc = existsSync(join(pkgRoot, "AGENT.md")) ? join(pkgRoot, "AGENT.md") : join(pkgRoot, "CLAUDE.md");
|
|
13
|
+
const metaSrc = existsSync(join(pkgRoot, "metadata.json")) ? join(pkgRoot, "metadata.json") : join(pkgRoot, ".claude-meta.json");
|
|
14
|
+
if (existsSync(agentMdSrc)) {
|
|
15
|
+
copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(metaSrc)) {
|
|
18
|
+
copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));
|
|
19
|
+
}
|
|
20
|
+
console.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);
|
|
21
|
+
//# sourceMappingURL=claude-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-context.js","sources":["../../src/cli/claude-context.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI script to install agent context for @happyvertical/jobs\n * Run the published context installer binary for this package.\n */\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst Dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgRoot = join(Dirname, '../..');\nconst targetDir = join(process.cwd(), '.claude');\n\nif (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true });\n}\n\nconst pkgName = 'jobs';\nconst agentMdSrc = existsSync(join(pkgRoot, 'AGENT.md'))\n ? join(pkgRoot, 'AGENT.md')\n : join(pkgRoot, 'CLAUDE.md');\nconst metaSrc = existsSync(join(pkgRoot, 'metadata.json'))\n ? join(pkgRoot, 'metadata.json')\n : join(pkgRoot, '.claude-meta.json');\n\nif (existsSync(agentMdSrc)) {\n copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));\n}\n\nif (existsSync(metaSrc)) {\n copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));\n}\n\nconsole.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);\n"],"names":[],"mappings":";;;;AASA,MAAM,UAAU,QAAQ,cAAc,YAAY,GAAG,CAAC;AACtD,MAAM,UAAU,KAAK,SAAS,OAAO;AACrC,MAAM,YAAY,KAAK,QAAQ,IAAA,GAAO,SAAS;AAE/C,IAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAU,WAAW,EAAE,WAAW,KAAA,CAAM;AAC1C;AAEA,MAAM,UAAU;AAChB,MAAM,aAAa,WAAW,KAAK,SAAS,UAAU,CAAC,IACnD,KAAK,SAAS,UAAU,IACxB,KAAK,SAAS,WAAW;AAC7B,MAAM,UAAU,WAAW,KAAK,SAAS,eAAe,CAAC,IACrD,KAAK,SAAS,eAAe,IAC7B,KAAK,SAAS,mBAAmB;AAErC,IAAI,WAAW,UAAU,GAAG;AAC1B,eAAa,YAAY,KAAK,WAAW,QAAQ,OAAO,KAAK,CAAC;AAChE;AAEA,IAAI,WAAW,OAAO,GAAG;AACvB,eAAa,SAAS,KAAK,WAAW,QAAQ,OAAO,YAAY,CAAC;AACpE;AAEA,QAAQ,IAAI,8BAA8B,OAAO,sBAAsB;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/jobs - Job queue abstraction with multiple backend adapters
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export { BullJobStore, type BullJobStoreConfig, type RedisOptions, } from './adapters/bull.js';
|
|
7
|
+
export { BullMQJobStore, type BullMQJobStoreConfig, } from './adapters/bullmq.js';
|
|
8
|
+
export { CloudTasksJobStore, type CloudTasksJobStoreConfig, } from './adapters/cloud-tasks.js';
|
|
9
|
+
export { PostgresJobStore, type PostgresJobStoreConfig, } from './adapters/postgres.js';
|
|
10
|
+
export { SqliteJobStore, type SqliteJobStoreConfig, } from './adapters/sqlite.js';
|
|
11
|
+
export { SQSJobStore, type SQSJobStoreConfig, } from './adapters/sqs.js';
|
|
12
|
+
export { BaseJobStore, priorityToNumber } from './base-store.js';
|
|
13
|
+
export { type CustomRetryFn, custom, DEFAULT_RETRY_STRATEGY, type ExponentialBackoffOptions, exponential, fromConfig, type LinearBackoffOptions, linear, noRetry, } from './retry.js';
|
|
14
|
+
export type { CleanupOptions, Job, JobCreateOptions, JobEvent, JobEventListener, JobEventType, JobFilter, JobHandle, JobHandler, JobPayload, JobPriority, JobStatus, JobStore, QueueStats, RetryDecision, RetryStrategy, RetryStrategyConfig, TimeoutBehavior, Unsubscribe, Worker, WorkerConfig, } from './types.js';
|
|
15
|
+
export { createWorker, JobWorker, type WorkerEvents } from './worker.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,kBAAkB,EAClB,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,gBAAgB,EAChB,KAAK,sBAAsB,GAC5B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,WAAW,EACX,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EACL,KAAK,aAAa,EAClB,MAAM,EACN,sBAAsB,EACtB,KAAK,yBAAyB,EAC9B,WAAW,EACX,UAAU,EACV,KAAK,oBAAoB,EACzB,MAAM,EACN,OAAO,GACR,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,cAAc,EACd,GAAG,EACH,gBAAgB,EAChB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,MAAM,EACN,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { BullJobStore } from "./adapters/bull.js";
|
|
2
|
+
import { BullMQJobStore } from "./adapters/bullmq.js";
|
|
3
|
+
import { CloudTasksJobStore } from "./adapters/cloud-tasks.js";
|
|
4
|
+
import { PostgresJobStore } from "./adapters/postgres.js";
|
|
5
|
+
import { SqliteJobStore } from "./adapters/sqlite.js";
|
|
6
|
+
import { SQSJobStore } from "./adapters/sqs.js";
|
|
7
|
+
import { f as fromConfig } from "./chunks/base-store-DlNksWvQ.js";
|
|
8
|
+
import { B, D, c, e, l, n, p } from "./chunks/base-store-DlNksWvQ.js";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { createId } from "@happyvertical/utils";
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
id: "",
|
|
13
|
+
concurrency: 5,
|
|
14
|
+
queues: ["default"],
|
|
15
|
+
pollInterval: 1e3,
|
|
16
|
+
heartbeatInterval: 3e4,
|
|
17
|
+
shutdownTimeout: 3e4
|
|
18
|
+
};
|
|
19
|
+
class JobWorker extends EventEmitter {
|
|
20
|
+
id;
|
|
21
|
+
store;
|
|
22
|
+
handler;
|
|
23
|
+
config;
|
|
24
|
+
running = false;
|
|
25
|
+
activeJobs = /* @__PURE__ */ new Map();
|
|
26
|
+
pollTimer = null;
|
|
27
|
+
heartbeatTimer = null;
|
|
28
|
+
shutdownPromise = null;
|
|
29
|
+
constructor(store, handler, config = {}) {
|
|
30
|
+
super();
|
|
31
|
+
this.store = store;
|
|
32
|
+
this.handler = handler;
|
|
33
|
+
this.config = {
|
|
34
|
+
...DEFAULT_CONFIG,
|
|
35
|
+
...config,
|
|
36
|
+
id: config.id || `worker_${createId().slice(0, 8)}`
|
|
37
|
+
};
|
|
38
|
+
this.id = this.config.id;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Start processing jobs
|
|
42
|
+
*/
|
|
43
|
+
async start() {
|
|
44
|
+
if (this.running) return;
|
|
45
|
+
this.running = true;
|
|
46
|
+
this.store.subscribe(async (event) => {
|
|
47
|
+
if (event.type === "job.ready" && this.running) {
|
|
48
|
+
await this.poll();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.startPolling();
|
|
52
|
+
this.startHeartbeat();
|
|
53
|
+
this.emit("worker:started");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stop processing jobs (graceful shutdown)
|
|
57
|
+
*/
|
|
58
|
+
async stop() {
|
|
59
|
+
if (!this.running) return;
|
|
60
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
61
|
+
this.running = false;
|
|
62
|
+
if (this.pollTimer) {
|
|
63
|
+
clearTimeout(this.pollTimer);
|
|
64
|
+
this.pollTimer = null;
|
|
65
|
+
}
|
|
66
|
+
if (this.heartbeatTimer) {
|
|
67
|
+
clearInterval(this.heartbeatTimer);
|
|
68
|
+
this.heartbeatTimer = null;
|
|
69
|
+
}
|
|
70
|
+
this.shutdownPromise = this.waitForActiveJobs();
|
|
71
|
+
try {
|
|
72
|
+
await this.shutdownPromise;
|
|
73
|
+
} finally {
|
|
74
|
+
this.shutdownPromise = null;
|
|
75
|
+
this.emit("worker:stopped");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if worker is running
|
|
80
|
+
*/
|
|
81
|
+
isRunning() {
|
|
82
|
+
return this.running;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get count of active jobs
|
|
86
|
+
*/
|
|
87
|
+
activeJobCount() {
|
|
88
|
+
return this.activeJobs.size;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Start the polling loop
|
|
92
|
+
*/
|
|
93
|
+
startPolling() {
|
|
94
|
+
const scheduleNextPoll = () => {
|
|
95
|
+
if (!this.running) return;
|
|
96
|
+
if (!this.store.waitForUpdate) {
|
|
97
|
+
this.pollTimer = setTimeout(poll, this.config.pollInterval);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.pollTimer = setTimeout(() => {
|
|
101
|
+
void waitThenPoll();
|
|
102
|
+
}, 0);
|
|
103
|
+
};
|
|
104
|
+
const waitThenPoll = async () => {
|
|
105
|
+
try {
|
|
106
|
+
await this.store.waitForUpdate?.(this.config.pollInterval);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.emit("worker:error", error);
|
|
109
|
+
}
|
|
110
|
+
if (this.running) {
|
|
111
|
+
await poll();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const poll = async () => {
|
|
115
|
+
if (!this.running) return;
|
|
116
|
+
try {
|
|
117
|
+
await this.poll();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.emit("worker:error", error);
|
|
120
|
+
}
|
|
121
|
+
scheduleNextPoll();
|
|
122
|
+
};
|
|
123
|
+
poll();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Poll for and process jobs
|
|
127
|
+
*/
|
|
128
|
+
async poll() {
|
|
129
|
+
const available = this.config.concurrency - this.activeJobs.size;
|
|
130
|
+
if (available <= 0) return;
|
|
131
|
+
const jobs = await this.store.dequeue(
|
|
132
|
+
this.config.queues,
|
|
133
|
+
available,
|
|
134
|
+
this.id
|
|
135
|
+
);
|
|
136
|
+
for (const job of jobs) {
|
|
137
|
+
this.processJob(job);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Process a single job
|
|
142
|
+
*/
|
|
143
|
+
async processJob(job) {
|
|
144
|
+
this.activeJobs.set(job.id, job);
|
|
145
|
+
this.emit("job:started", job);
|
|
146
|
+
let timeoutId = null;
|
|
147
|
+
try {
|
|
148
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
149
|
+
timeoutId = setTimeout(() => {
|
|
150
|
+
reject(new Error(`Job timeout after ${job.timeout}ms`));
|
|
151
|
+
}, job.timeout);
|
|
152
|
+
});
|
|
153
|
+
const result = await Promise.race([this.handler(job), timeoutPromise]);
|
|
154
|
+
await this.store.update(job.id, {
|
|
155
|
+
status: "completed",
|
|
156
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
157
|
+
resultPointer: result.resultPointer ?? null
|
|
158
|
+
});
|
|
159
|
+
this.emit("job:completed", job, result.result);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
await this.handleJobError(job, error);
|
|
162
|
+
} finally {
|
|
163
|
+
if (timeoutId) {
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
}
|
|
166
|
+
this.activeJobs.delete(job.id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Handle job execution error
|
|
171
|
+
*/
|
|
172
|
+
async handleJobError(job, error) {
|
|
173
|
+
const strategy = fromConfig(job.retryStrategy);
|
|
174
|
+
const decision = strategy.shouldRetry(job.attempts, error);
|
|
175
|
+
if (decision.shouldRetry && job.attempts < job.maxAttempts) {
|
|
176
|
+
const nextRunAt = new Date(Date.now() + decision.delay);
|
|
177
|
+
await this.store.update(job.id, {
|
|
178
|
+
status: "pending",
|
|
179
|
+
lastError: error.message,
|
|
180
|
+
runAt: nextRunAt,
|
|
181
|
+
workerId: null,
|
|
182
|
+
workerHeartbeat: null
|
|
183
|
+
});
|
|
184
|
+
this.emit("job:retrying", job, error, decision.delay);
|
|
185
|
+
} else {
|
|
186
|
+
await this.store.update(job.id, {
|
|
187
|
+
status: "failed",
|
|
188
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
189
|
+
lastError: error.message
|
|
190
|
+
});
|
|
191
|
+
this.emit("job:failed", job, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Start heartbeat loop to keep jobs alive
|
|
196
|
+
*/
|
|
197
|
+
startHeartbeat() {
|
|
198
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
199
|
+
for (const [jobId] of this.activeJobs) {
|
|
200
|
+
try {
|
|
201
|
+
await this.store.heartbeat(jobId, this.id);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn(`Heartbeat failed for job ${jobId}:`, error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, this.config.heartbeatInterval);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Wait for active jobs to complete with timeout
|
|
210
|
+
*/
|
|
211
|
+
async waitForActiveJobs() {
|
|
212
|
+
if (this.activeJobs.size === 0) return;
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
const checkInterval = setInterval(() => {
|
|
215
|
+
if (this.activeJobs.size === 0) {
|
|
216
|
+
clearInterval(checkInterval);
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
resolve();
|
|
219
|
+
}
|
|
220
|
+
}, 100);
|
|
221
|
+
const timeout = setTimeout(() => {
|
|
222
|
+
clearInterval(checkInterval);
|
|
223
|
+
console.warn(
|
|
224
|
+
`Shutdown timeout: ${this.activeJobs.size} jobs still active`
|
|
225
|
+
);
|
|
226
|
+
resolve();
|
|
227
|
+
}, this.config.shutdownTimeout);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function createWorker(store, handler, config) {
|
|
232
|
+
return new JobWorker(store, handler, config);
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
B as BaseJobStore,
|
|
236
|
+
BullJobStore,
|
|
237
|
+
BullMQJobStore,
|
|
238
|
+
CloudTasksJobStore,
|
|
239
|
+
D as DEFAULT_RETRY_STRATEGY,
|
|
240
|
+
JobWorker,
|
|
241
|
+
PostgresJobStore,
|
|
242
|
+
SQSJobStore,
|
|
243
|
+
SqliteJobStore,
|
|
244
|
+
createWorker,
|
|
245
|
+
c as custom,
|
|
246
|
+
e as exponential,
|
|
247
|
+
fromConfig,
|
|
248
|
+
l as linear,
|
|
249
|
+
n as noRetry,
|
|
250
|
+
p as priorityToNumber
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=index.js.map
|