workmatic 1.0.5 → 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/README.md +114 -13
- package/dashboard/app.js +1 -3
- package/dashboard/index.html +0 -14
- package/dist/cli.cjs +905 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +905 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +360 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -6
- package/dist/index.d.ts +118 -6
- package/dist/index.js +357 -66
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -30,12 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
DEFAULT_WORKER_TIMEOUT_MS: () => DEFAULT_WORKER_TIMEOUT_MS,
|
|
33
34
|
createClient: () => createClient,
|
|
34
35
|
createDashboard: () => createDashboard,
|
|
35
36
|
createDashboardMiddleware: () => createDashboardMiddleware,
|
|
36
37
|
createDatabase: () => createDatabase,
|
|
38
|
+
createOrchestrator: () => createOrchestrator,
|
|
37
39
|
createWorker: () => createWorker,
|
|
38
40
|
defaultBackoff: () => defaultBackoff,
|
|
41
|
+
getUnderlyingDb: () => getUnderlyingDb,
|
|
39
42
|
validatePayload: () => validatePayload
|
|
40
43
|
});
|
|
41
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -47,6 +50,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
47
50
|
// src/database.ts
|
|
48
51
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
49
52
|
var import_kysely = require("kysely");
|
|
53
|
+
var kyselyToSqlite = /* @__PURE__ */ new WeakMap();
|
|
50
54
|
function createDatabase(options = {}) {
|
|
51
55
|
let sqliteDb;
|
|
52
56
|
if (options.db) {
|
|
@@ -64,6 +68,7 @@ function createDatabase(options = {}) {
|
|
|
64
68
|
})
|
|
65
69
|
});
|
|
66
70
|
createSchema(sqliteDb);
|
|
71
|
+
kyselyToSqlite.set(db, sqliteDb);
|
|
67
72
|
return db;
|
|
68
73
|
}
|
|
69
74
|
function createSchema(db) {
|
|
@@ -99,6 +104,26 @@ function createSchema(db) {
|
|
|
99
104
|
updated_at INTEGER NOT NULL
|
|
100
105
|
)
|
|
101
106
|
`);
|
|
107
|
+
db.exec(`
|
|
108
|
+
UPDATE workmatic_jobs SET status = 'dead' WHERE status = 'failed'
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
function getUnderlyingDb(db) {
|
|
112
|
+
const mapped = kyselyToSqlite.get(db);
|
|
113
|
+
if (mapped) {
|
|
114
|
+
return mapped;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const ex = db.getExecutor?.();
|
|
118
|
+
const dialect = ex?.adapter?.db;
|
|
119
|
+
if (dialect) {
|
|
120
|
+
return dialect;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
throw new Error(
|
|
125
|
+
"getUnderlyingDb: could not resolve better-sqlite3 instance (use createDatabase() or pass db from it)"
|
|
126
|
+
);
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
// src/client.ts
|
|
@@ -167,6 +192,42 @@ function createClient(options) {
|
|
|
167
192
|
}).execute();
|
|
168
193
|
return { ok: true, id: publicId };
|
|
169
194
|
},
|
|
195
|
+
async addMany(payloads, opts = {}) {
|
|
196
|
+
const {
|
|
197
|
+
priority = 0,
|
|
198
|
+
delayMs = 0,
|
|
199
|
+
maxAttempts = 3
|
|
200
|
+
} = opts;
|
|
201
|
+
if (payloads.length === 0) {
|
|
202
|
+
return { ok: true, ids: [] };
|
|
203
|
+
}
|
|
204
|
+
const timestamp = now();
|
|
205
|
+
const runAt = timestamp + delayMs;
|
|
206
|
+
return await db.transaction().execute(async (trx) => {
|
|
207
|
+
const ids = [];
|
|
208
|
+
const rows = payloads.map((payload) => {
|
|
209
|
+
const payloadJson = validatePayload(payload);
|
|
210
|
+
const publicId = (0, import_nanoid.nanoid)();
|
|
211
|
+
ids.push(publicId);
|
|
212
|
+
return {
|
|
213
|
+
public_id: publicId,
|
|
214
|
+
queue,
|
|
215
|
+
payload: payloadJson,
|
|
216
|
+
status: "ready",
|
|
217
|
+
priority,
|
|
218
|
+
run_at: runAt,
|
|
219
|
+
attempts: 0,
|
|
220
|
+
max_attempts: maxAttempts,
|
|
221
|
+
lease_until: 0,
|
|
222
|
+
created_at: timestamp,
|
|
223
|
+
updated_at: timestamp,
|
|
224
|
+
last_error: null
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
await trx.insertInto("workmatic_jobs").values(rows).execute();
|
|
228
|
+
return { ok: true, ids };
|
|
229
|
+
});
|
|
230
|
+
},
|
|
170
231
|
/**
|
|
171
232
|
* Get job statistics for the queue
|
|
172
233
|
*/
|
|
@@ -179,7 +240,6 @@ function createClient(options) {
|
|
|
179
240
|
ready: 0,
|
|
180
241
|
running: 0,
|
|
181
242
|
done: 0,
|
|
182
|
-
failed: 0,
|
|
183
243
|
dead: 0,
|
|
184
244
|
total: 0
|
|
185
245
|
};
|
|
@@ -210,6 +270,30 @@ function createClient(options) {
|
|
|
210
270
|
// src/worker.ts
|
|
211
271
|
var import_fastq = __toESM(require("fastq"), 1);
|
|
212
272
|
var import_kysely3 = require("kysely");
|
|
273
|
+
var DEFAULT_WORKER_TIMEOUT_MS = 6e4;
|
|
274
|
+
function parseClaimedRows(result) {
|
|
275
|
+
const rows = result.rows;
|
|
276
|
+
if (!rows || !Array.isArray(rows)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
return rows.map((row) => ({
|
|
280
|
+
id: row.id,
|
|
281
|
+
public_id: row.public_id,
|
|
282
|
+
queue: row.queue,
|
|
283
|
+
payload: row.payload,
|
|
284
|
+
attempts: row.attempts,
|
|
285
|
+
max_attempts: row.max_attempts,
|
|
286
|
+
priority: row.priority,
|
|
287
|
+
created_at: row.created_at,
|
|
288
|
+
last_error: row.last_error
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
function requireProcessor(processor) {
|
|
292
|
+
if (!processor) {
|
|
293
|
+
throw new Error("No processor set");
|
|
294
|
+
}
|
|
295
|
+
return processor;
|
|
296
|
+
}
|
|
213
297
|
function createWorker(options) {
|
|
214
298
|
const {
|
|
215
299
|
db,
|
|
@@ -217,10 +301,13 @@ function createWorker(options) {
|
|
|
217
301
|
concurrency = 1,
|
|
218
302
|
leaseMs = 3e4,
|
|
219
303
|
pollMs = 1e3,
|
|
220
|
-
timeoutMs,
|
|
304
|
+
timeoutMs = DEFAULT_WORKER_TIMEOUT_MS,
|
|
221
305
|
backoff = defaultBackoff,
|
|
222
306
|
persistState = false,
|
|
223
|
-
autoRestore = true
|
|
307
|
+
autoRestore = true,
|
|
308
|
+
pauseCheckIntervalMs = 300,
|
|
309
|
+
requeueExpiredIntervalMs = 0,
|
|
310
|
+
onPumpError
|
|
224
311
|
} = options;
|
|
225
312
|
if (!db) {
|
|
226
313
|
throw new Error("Database instance is required");
|
|
@@ -230,6 +317,13 @@ function createWorker(options) {
|
|
|
230
317
|
let processor = null;
|
|
231
318
|
let pumpTimeout = null;
|
|
232
319
|
let fastqQueue = null;
|
|
320
|
+
let lastPauseCheckAt = 0;
|
|
321
|
+
let cachedDbPaused = false;
|
|
322
|
+
let lastRequeueAt = 0;
|
|
323
|
+
function notifyPumpError(error) {
|
|
324
|
+
console.error("[workmatic] Pump error:", error);
|
|
325
|
+
onPumpError?.(error);
|
|
326
|
+
}
|
|
233
327
|
function getStateKey() {
|
|
234
328
|
return `worker_state_${queue}`;
|
|
235
329
|
}
|
|
@@ -237,8 +331,6 @@ function createWorker(options) {
|
|
|
237
331
|
if (!persistState) return;
|
|
238
332
|
const timestamp = now();
|
|
239
333
|
const key = getStateKey();
|
|
240
|
-
await db.schema.createTable("workmatic_settings").ifNotExists().addColumn("queue", "text", (col) => col.primaryKey()).addColumn("paused", "integer", (col) => col.notNull().defaultTo(0)).addColumn("updated_at", "integer", (col) => col.notNull()).execute().catch(() => {
|
|
241
|
-
});
|
|
242
334
|
await import_kysely3.sql`
|
|
243
335
|
INSERT INTO workmatic_settings (queue, paused, updated_at)
|
|
244
336
|
VALUES (${key}, ${state === "paused" ? 1 : state === "running" ? 2 : 0}, ${timestamp})
|
|
@@ -273,17 +365,20 @@ function createWorker(options) {
|
|
|
273
365
|
const timestamp = now();
|
|
274
366
|
const leaseUntil = timestamp + leaseMs;
|
|
275
367
|
return await db.transaction().execute(async (trx) => {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
368
|
+
const result = await import_kysely3.sql`
|
|
369
|
+
UPDATE workmatic_jobs
|
|
370
|
+
SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
|
|
371
|
+
WHERE rowid IN (
|
|
372
|
+
SELECT rowid FROM workmatic_jobs
|
|
373
|
+
WHERE queue = ${queue}
|
|
374
|
+
AND status = 'ready'
|
|
375
|
+
AND run_at <= ${timestamp}
|
|
376
|
+
ORDER BY priority ASC, id ASC
|
|
377
|
+
LIMIT ${limit}
|
|
378
|
+
)
|
|
379
|
+
RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
|
|
380
|
+
`.execute(trx);
|
|
381
|
+
return parseClaimedRows(result);
|
|
287
382
|
});
|
|
288
383
|
}
|
|
289
384
|
async function markDone(jobId) {
|
|
@@ -331,28 +426,24 @@ function createWorker(options) {
|
|
|
331
426
|
}
|
|
332
427
|
}
|
|
333
428
|
async function processJob(claimedJob) {
|
|
334
|
-
|
|
335
|
-
throw new Error("No processor set");
|
|
336
|
-
}
|
|
429
|
+
const fn = requireProcessor(processor);
|
|
337
430
|
const payload = parsePayload(claimedJob.payload);
|
|
338
431
|
const job = {
|
|
339
432
|
id: claimedJob.public_id,
|
|
340
433
|
queue: claimedJob.queue,
|
|
341
434
|
payload,
|
|
342
435
|
status: "running",
|
|
343
|
-
priority:
|
|
344
|
-
// Not needed for processing
|
|
436
|
+
priority: claimedJob.priority,
|
|
345
437
|
attempts: claimedJob.attempts,
|
|
346
438
|
maxAttempts: claimedJob.max_attempts,
|
|
347
|
-
createdAt:
|
|
348
|
-
|
|
349
|
-
lastError: null
|
|
439
|
+
createdAt: claimedJob.created_at,
|
|
440
|
+
lastError: claimedJob.last_error
|
|
350
441
|
};
|
|
351
442
|
try {
|
|
352
443
|
if (timeoutMs) {
|
|
353
|
-
await withTimeout(
|
|
444
|
+
await withTimeout(fn(job), timeoutMs, job.id);
|
|
354
445
|
} else {
|
|
355
|
-
await
|
|
446
|
+
await fn(job);
|
|
356
447
|
}
|
|
357
448
|
await markDone(claimedJob.id);
|
|
358
449
|
} catch (error) {
|
|
@@ -377,12 +468,21 @@ function createWorker(options) {
|
|
|
377
468
|
return;
|
|
378
469
|
}
|
|
379
470
|
try {
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
471
|
+
const t = now();
|
|
472
|
+
if (t - lastPauseCheckAt >= pauseCheckIntervalMs) {
|
|
473
|
+
lastPauseCheckAt = t;
|
|
474
|
+
cachedDbPaused = await isQueuePausedInDb();
|
|
475
|
+
}
|
|
476
|
+
if (cachedDbPaused) {
|
|
382
477
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
383
478
|
return;
|
|
384
479
|
}
|
|
385
|
-
|
|
480
|
+
if (requeueExpiredIntervalMs <= 0 || t - lastRequeueAt >= requeueExpiredIntervalMs) {
|
|
481
|
+
if (requeueExpiredIntervalMs > 0) {
|
|
482
|
+
lastRequeueAt = t;
|
|
483
|
+
}
|
|
484
|
+
await requeueExpiredLeases();
|
|
485
|
+
}
|
|
386
486
|
const batchSize = concurrency * 2;
|
|
387
487
|
const jobs = await claimBatch(batchSize);
|
|
388
488
|
if (jobs.length > 0) {
|
|
@@ -394,7 +494,7 @@ function createWorker(options) {
|
|
|
394
494
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
395
495
|
}
|
|
396
496
|
} catch (error) {
|
|
397
|
-
|
|
497
|
+
notifyPumpError(error);
|
|
398
498
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
399
499
|
}
|
|
400
500
|
}
|
|
@@ -450,7 +550,6 @@ function createWorker(options) {
|
|
|
450
550
|
ready: 0,
|
|
451
551
|
running: 0,
|
|
452
552
|
done: 0,
|
|
453
|
-
failed: 0,
|
|
454
553
|
dead: 0,
|
|
455
554
|
total: 0
|
|
456
555
|
};
|
|
@@ -502,12 +601,211 @@ function createWorker(options) {
|
|
|
502
601
|
return worker;
|
|
503
602
|
}
|
|
504
603
|
|
|
604
|
+
// src/orchestrator.ts
|
|
605
|
+
var import_kysely4 = require("kysely");
|
|
606
|
+
var DEFAULT_TRANSFER_STATUSES = ["ready", "dead"];
|
|
607
|
+
var DEFAULT_TRANSFER_LIMIT = 1e4;
|
|
608
|
+
function rowsUpdated(result) {
|
|
609
|
+
const row = result[0];
|
|
610
|
+
return Number(row?.numUpdatedRows ?? 0);
|
|
611
|
+
}
|
|
612
|
+
function normalizeStatuses(status) {
|
|
613
|
+
if (!status) return [...DEFAULT_TRANSFER_STATUSES];
|
|
614
|
+
return Array.isArray(status) ? status : [status];
|
|
615
|
+
}
|
|
616
|
+
function createOrchestrator(options) {
|
|
617
|
+
const { db } = options;
|
|
618
|
+
if (!db) {
|
|
619
|
+
throw new Error("Database instance is required");
|
|
620
|
+
}
|
|
621
|
+
const registry = /* @__PURE__ */ new Map();
|
|
622
|
+
function ensureEntry(queue) {
|
|
623
|
+
let entry = registry.get(queue);
|
|
624
|
+
if (!entry) {
|
|
625
|
+
entry = { client: createClient({ db, queue }) };
|
|
626
|
+
registry.set(queue, entry);
|
|
627
|
+
}
|
|
628
|
+
return entry;
|
|
629
|
+
}
|
|
630
|
+
async function setQueuePaused(queue, paused) {
|
|
631
|
+
const timestamp = now();
|
|
632
|
+
const value = paused ? 1 : 0;
|
|
633
|
+
await import_kysely4.sql`
|
|
634
|
+
INSERT INTO workmatic_settings (queue, paused, updated_at)
|
|
635
|
+
VALUES (${queue}, ${value}, ${timestamp})
|
|
636
|
+
ON CONFLICT(queue) DO UPDATE SET
|
|
637
|
+
paused = ${value},
|
|
638
|
+
updated_at = ${timestamp}
|
|
639
|
+
`.execute(db);
|
|
640
|
+
}
|
|
641
|
+
async function selectJobIds(from, status, limit) {
|
|
642
|
+
const rows = await db.selectFrom("workmatic_jobs").select("id").where("queue", "=", from).where("status", "=", status).orderBy("priority", "asc").orderBy("id", "asc").limit(limit).execute();
|
|
643
|
+
return rows.map((r) => r.id);
|
|
644
|
+
}
|
|
645
|
+
async function transferReady(from, to, limit) {
|
|
646
|
+
const ids = await selectJobIds(from, "ready", limit);
|
|
647
|
+
if (ids.length === 0) return 0;
|
|
648
|
+
const timestamp = now();
|
|
649
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
650
|
+
return rowsUpdated(result);
|
|
651
|
+
}
|
|
652
|
+
async function transferDead(from, to, limit, resetForRetry) {
|
|
653
|
+
const ids = await selectJobIds(from, "dead", limit);
|
|
654
|
+
if (ids.length === 0) return 0;
|
|
655
|
+
const timestamp = now();
|
|
656
|
+
if (resetForRetry) {
|
|
657
|
+
const result2 = await db.updateTable("workmatic_jobs").set({
|
|
658
|
+
queue: to,
|
|
659
|
+
status: "ready",
|
|
660
|
+
attempts: 0,
|
|
661
|
+
lease_until: 0,
|
|
662
|
+
last_error: null,
|
|
663
|
+
updated_at: timestamp,
|
|
664
|
+
run_at: timestamp
|
|
665
|
+
}).where("id", "in", ids).execute();
|
|
666
|
+
return rowsUpdated(result2);
|
|
667
|
+
}
|
|
668
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
669
|
+
return rowsUpdated(result);
|
|
670
|
+
}
|
|
671
|
+
async function transferOtherStatus(from, to, status, limit) {
|
|
672
|
+
const ids = await selectJobIds(from, status, limit);
|
|
673
|
+
if (ids.length === 0) return 0;
|
|
674
|
+
const timestamp = now();
|
|
675
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
676
|
+
return rowsUpdated(result);
|
|
677
|
+
}
|
|
678
|
+
const orchestrator = {
|
|
679
|
+
register(queue, opts = {}) {
|
|
680
|
+
const client = createClient({ db, queue });
|
|
681
|
+
let worker;
|
|
682
|
+
if (opts.worker) {
|
|
683
|
+
worker = createWorker({ db, queue, ...opts.worker });
|
|
684
|
+
}
|
|
685
|
+
registry.set(queue, { client, worker });
|
|
686
|
+
return client;
|
|
687
|
+
},
|
|
688
|
+
client(queue) {
|
|
689
|
+
return ensureEntry(queue).client;
|
|
690
|
+
},
|
|
691
|
+
worker(queue) {
|
|
692
|
+
const entry = registry.get(queue);
|
|
693
|
+
if (!entry?.worker) {
|
|
694
|
+
throw new Error(`No worker registered for queue "${queue}"`);
|
|
695
|
+
}
|
|
696
|
+
return entry.worker;
|
|
697
|
+
},
|
|
698
|
+
workers() {
|
|
699
|
+
return [...registry.values()].map((e) => e.worker).filter((w) => w !== void 0);
|
|
700
|
+
},
|
|
701
|
+
async queues() {
|
|
702
|
+
const rows = await db.selectFrom("workmatic_jobs").select("queue").distinct().execute();
|
|
703
|
+
const names = new Set(rows.map((r) => r.queue));
|
|
704
|
+
for (const name of registry.keys()) {
|
|
705
|
+
names.add(name);
|
|
706
|
+
}
|
|
707
|
+
return [...names].sort();
|
|
708
|
+
},
|
|
709
|
+
process(queue, fn) {
|
|
710
|
+
this.worker(queue).process(fn);
|
|
711
|
+
},
|
|
712
|
+
startAll() {
|
|
713
|
+
for (const entry of registry.values()) {
|
|
714
|
+
entry.worker?.start();
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
async stopAll() {
|
|
718
|
+
const stops = [...registry.values()].map((e) => e.worker?.stop()).filter((p) => p !== void 0);
|
|
719
|
+
await Promise.all(stops);
|
|
720
|
+
},
|
|
721
|
+
async pause(queue) {
|
|
722
|
+
const entry = registry.get(queue);
|
|
723
|
+
entry?.worker?.pause();
|
|
724
|
+
await setQueuePaused(queue, true);
|
|
725
|
+
},
|
|
726
|
+
async resume(queue) {
|
|
727
|
+
const entry = registry.get(queue);
|
|
728
|
+
entry?.worker?.resume();
|
|
729
|
+
await setQueuePaused(queue, false);
|
|
730
|
+
},
|
|
731
|
+
async isPaused(queue) {
|
|
732
|
+
const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
|
|
733
|
+
return setting?.paused === 1;
|
|
734
|
+
},
|
|
735
|
+
async stats(queueName) {
|
|
736
|
+
const names = queueName ? [queueName] : await this.queues();
|
|
737
|
+
const result = {};
|
|
738
|
+
for (const name of names) {
|
|
739
|
+
result[name] = await ensureEntry(name).client.stats();
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
},
|
|
743
|
+
async transfer(opts) {
|
|
744
|
+
const { from, to, resetForRetry = false } = opts;
|
|
745
|
+
if (from === to) {
|
|
746
|
+
return { moved: 0 };
|
|
747
|
+
}
|
|
748
|
+
const statuses = normalizeStatuses(opts.status);
|
|
749
|
+
let remaining = opts.limit ?? DEFAULT_TRANSFER_LIMIT;
|
|
750
|
+
let moved = 0;
|
|
751
|
+
if (statuses.includes("ready") && remaining > 0) {
|
|
752
|
+
const n = await transferReady(from, to, remaining);
|
|
753
|
+
moved += n;
|
|
754
|
+
remaining -= n;
|
|
755
|
+
}
|
|
756
|
+
if (statuses.includes("dead") && remaining > 0) {
|
|
757
|
+
const n = await transferDead(from, to, remaining, resetForRetry);
|
|
758
|
+
moved += n;
|
|
759
|
+
remaining -= n;
|
|
760
|
+
}
|
|
761
|
+
for (const status of statuses) {
|
|
762
|
+
if (status === "ready" || status === "dead" || remaining <= 0) continue;
|
|
763
|
+
const n = await transferOtherStatus(from, to, status, remaining);
|
|
764
|
+
moved += n;
|
|
765
|
+
remaining -= n;
|
|
766
|
+
}
|
|
767
|
+
return { moved };
|
|
768
|
+
},
|
|
769
|
+
async moveJob(publicId, toQueue, opts = {}) {
|
|
770
|
+
const allowed = normalizeStatuses(opts.status);
|
|
771
|
+
const row = await db.selectFrom("workmatic_jobs").select(["id", "queue", "status"]).where("public_id", "=", publicId).executeTakeFirst();
|
|
772
|
+
if (!row) {
|
|
773
|
+
throw new Error(`Job not found: ${publicId}`);
|
|
774
|
+
}
|
|
775
|
+
if (row.queue === toQueue) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (!allowed.includes(row.status)) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`Job ${publicId} has status "${row.status}" and cannot be moved (allowed: ${allowed.join(", ")})`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const timestamp = now();
|
|
784
|
+
const resetForRetry = opts.resetForRetry ?? false;
|
|
785
|
+
if (row.status === "dead" && resetForRetry) {
|
|
786
|
+
await db.updateTable("workmatic_jobs").set({
|
|
787
|
+
queue: toQueue,
|
|
788
|
+
status: "ready",
|
|
789
|
+
attempts: 0,
|
|
790
|
+
lease_until: 0,
|
|
791
|
+
last_error: null,
|
|
792
|
+
updated_at: timestamp,
|
|
793
|
+
run_at: timestamp
|
|
794
|
+
}).where("public_id", "=", publicId).execute();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
await db.updateTable("workmatic_jobs").set({ queue: toQueue, updated_at: timestamp }).where("public_id", "=", publicId).execute();
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
return orchestrator;
|
|
801
|
+
}
|
|
802
|
+
|
|
505
803
|
// src/dashboard.ts
|
|
506
804
|
var import_http = require("http");
|
|
507
805
|
var import_url = require("url");
|
|
508
806
|
var import_path = require("path");
|
|
509
807
|
var import_promises = require("fs/promises");
|
|
510
|
-
var
|
|
808
|
+
var import_kysely5 = require("kysely");
|
|
511
809
|
var __filename2 = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
512
810
|
var __dirname = (0, import_path.dirname)(__filename2);
|
|
513
811
|
var CONTENT_TYPES = {
|
|
@@ -516,6 +814,13 @@ var CONTENT_TYPES = {
|
|
|
516
814
|
".js": "application/javascript; charset=utf-8",
|
|
517
815
|
".json": "application/json; charset=utf-8"
|
|
518
816
|
};
|
|
817
|
+
function requestUrl(url) {
|
|
818
|
+
return url || "";
|
|
819
|
+
}
|
|
820
|
+
function staticContentTypeFor(filePath) {
|
|
821
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")) || ".html";
|
|
822
|
+
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
823
|
+
}
|
|
519
824
|
function createRequestHandler(db, workerMap, basePath = "") {
|
|
520
825
|
function sendJson(res, data, status = 200) {
|
|
521
826
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -538,7 +843,7 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
538
843
|
return path;
|
|
539
844
|
}
|
|
540
845
|
async function handleGetJobs(req, res) {
|
|
541
|
-
const query = parseQuery(req.url
|
|
846
|
+
const query = parseQuery(requestUrl(req.url));
|
|
542
847
|
const queueFilter = query.get("queue");
|
|
543
848
|
const statusFilter = query.get("status");
|
|
544
849
|
const limit = Math.min(parseInt(query.get("limit") || "50", 10), 100);
|
|
@@ -579,11 +884,11 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
579
884
|
sendJson(res, { jobs: apiJobs, limit, offset });
|
|
580
885
|
}
|
|
581
886
|
async function handleGetStats(req, res) {
|
|
582
|
-
const query = parseQuery(req.url
|
|
887
|
+
const query = parseQuery(requestUrl(req.url));
|
|
583
888
|
const queueFilter = query.get("queue");
|
|
584
889
|
let statsQuery = db.selectFrom("workmatic_jobs").select([
|
|
585
890
|
"status",
|
|
586
|
-
|
|
891
|
+
import_kysely5.sql`count(*)`.as("count")
|
|
587
892
|
]).groupBy("status");
|
|
588
893
|
if (queueFilter) {
|
|
589
894
|
statsQuery = statsQuery.where("queue", "=", queueFilter);
|
|
@@ -593,7 +898,6 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
593
898
|
ready: 0,
|
|
594
899
|
running: 0,
|
|
595
900
|
done: 0,
|
|
596
|
-
failed: 0,
|
|
597
901
|
dead: 0,
|
|
598
902
|
total: 0
|
|
599
903
|
};
|
|
@@ -666,8 +970,7 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
666
970
|
async function serveStatic(res, filePath) {
|
|
667
971
|
const dashboardDir = (0, import_path.join)(__dirname, "..", "dashboard");
|
|
668
972
|
const fullPath = (0, import_path.join)(dashboardDir, filePath);
|
|
669
|
-
const
|
|
670
|
-
const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
|
|
973
|
+
const contentType = staticContentTypeFor(filePath);
|
|
671
974
|
try {
|
|
672
975
|
const content = await (0, import_promises.readFile)(fullPath, "utf-8");
|
|
673
976
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -677,21 +980,21 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
677
980
|
}
|
|
678
981
|
}
|
|
679
982
|
return async function handleRequest(req, res, next) {
|
|
680
|
-
const path = getPath(req.url || "/");
|
|
681
|
-
const fullUrl = req.url || "/";
|
|
682
|
-
if (basePath && !fullUrl.startsWith(basePath)) {
|
|
683
|
-
if (next) next();
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
687
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
688
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
689
|
-
if (req.method === "OPTIONS") {
|
|
690
|
-
res.writeHead(204);
|
|
691
|
-
res.end();
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
983
|
try {
|
|
984
|
+
const path = getPath(req.url || "/");
|
|
985
|
+
const fullUrl = req.url || "/";
|
|
986
|
+
if (basePath && !fullUrl.startsWith(basePath)) {
|
|
987
|
+
if (next) next();
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
991
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
992
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
993
|
+
if (req.method === "OPTIONS") {
|
|
994
|
+
res.writeHead(204);
|
|
995
|
+
res.end();
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
695
998
|
if (path === "/api/jobs" && req.method === "GET") {
|
|
696
999
|
await handleGetJobs(req, res);
|
|
697
1000
|
return true;
|
|
@@ -751,11 +1054,7 @@ function createDashboard(options) {
|
|
|
751
1054
|
}
|
|
752
1055
|
const handleRequest = createRequestHandler(db, workerMap);
|
|
753
1056
|
const server = (0, import_http.createServer)((req, res) => {
|
|
754
|
-
handleRequest(req, res)
|
|
755
|
-
console.error("[workmatic] Unhandled error:", error);
|
|
756
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
757
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
758
|
-
});
|
|
1057
|
+
void handleRequest(req, res);
|
|
759
1058
|
});
|
|
760
1059
|
server.listen(port);
|
|
761
1060
|
return {
|
|
@@ -787,25 +1086,20 @@ function createDashboardMiddleware(options) {
|
|
|
787
1086
|
}
|
|
788
1087
|
const handleRequest = createRequestHandler(db, workerMap, basePath);
|
|
789
1088
|
return (req, res, next) => {
|
|
790
|
-
handleRequest(req, res, next)
|
|
791
|
-
console.error("[workmatic] Unhandled error:", error);
|
|
792
|
-
if (next) {
|
|
793
|
-
next();
|
|
794
|
-
} else {
|
|
795
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
796
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
797
|
-
}
|
|
798
|
-
});
|
|
1089
|
+
void handleRequest(req, res, next);
|
|
799
1090
|
};
|
|
800
1091
|
}
|
|
801
1092
|
// Annotate the CommonJS export names for ESM import in node:
|
|
802
1093
|
0 && (module.exports = {
|
|
1094
|
+
DEFAULT_WORKER_TIMEOUT_MS,
|
|
803
1095
|
createClient,
|
|
804
1096
|
createDashboard,
|
|
805
1097
|
createDashboardMiddleware,
|
|
806
1098
|
createDatabase,
|
|
1099
|
+
createOrchestrator,
|
|
807
1100
|
createWorker,
|
|
808
1101
|
defaultBackoff,
|
|
1102
|
+
getUnderlyingDb,
|
|
809
1103
|
validatePayload
|
|
810
1104
|
});
|
|
811
1105
|
//# sourceMappingURL=index.cjs.map
|