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.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/database.ts
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import { Kysely, SqliteDialect } from "kysely";
|
|
4
|
+
var kyselyToSqlite = /* @__PURE__ */ new WeakMap();
|
|
4
5
|
function createDatabase(options = {}) {
|
|
5
6
|
let sqliteDb;
|
|
6
7
|
if (options.db) {
|
|
@@ -18,6 +19,7 @@ function createDatabase(options = {}) {
|
|
|
18
19
|
})
|
|
19
20
|
});
|
|
20
21
|
createSchema(sqliteDb);
|
|
22
|
+
kyselyToSqlite.set(db, sqliteDb);
|
|
21
23
|
return db;
|
|
22
24
|
}
|
|
23
25
|
function createSchema(db) {
|
|
@@ -53,6 +55,26 @@ function createSchema(db) {
|
|
|
53
55
|
updated_at INTEGER NOT NULL
|
|
54
56
|
)
|
|
55
57
|
`);
|
|
58
|
+
db.exec(`
|
|
59
|
+
UPDATE workmatic_jobs SET status = 'dead' WHERE status = 'failed'
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
function getUnderlyingDb(db) {
|
|
63
|
+
const mapped = kyselyToSqlite.get(db);
|
|
64
|
+
if (mapped) {
|
|
65
|
+
return mapped;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const ex = db.getExecutor?.();
|
|
69
|
+
const dialect = ex?.adapter?.db;
|
|
70
|
+
if (dialect) {
|
|
71
|
+
return dialect;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
throw new Error(
|
|
76
|
+
"getUnderlyingDb: could not resolve better-sqlite3 instance (use createDatabase() or pass db from it)"
|
|
77
|
+
);
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
// src/client.ts
|
|
@@ -121,6 +143,42 @@ function createClient(options) {
|
|
|
121
143
|
}).execute();
|
|
122
144
|
return { ok: true, id: publicId };
|
|
123
145
|
},
|
|
146
|
+
async addMany(payloads, opts = {}) {
|
|
147
|
+
const {
|
|
148
|
+
priority = 0,
|
|
149
|
+
delayMs = 0,
|
|
150
|
+
maxAttempts = 3
|
|
151
|
+
} = opts;
|
|
152
|
+
if (payloads.length === 0) {
|
|
153
|
+
return { ok: true, ids: [] };
|
|
154
|
+
}
|
|
155
|
+
const timestamp = now();
|
|
156
|
+
const runAt = timestamp + delayMs;
|
|
157
|
+
return await db.transaction().execute(async (trx) => {
|
|
158
|
+
const ids = [];
|
|
159
|
+
const rows = payloads.map((payload) => {
|
|
160
|
+
const payloadJson = validatePayload(payload);
|
|
161
|
+
const publicId = nanoid();
|
|
162
|
+
ids.push(publicId);
|
|
163
|
+
return {
|
|
164
|
+
public_id: publicId,
|
|
165
|
+
queue,
|
|
166
|
+
payload: payloadJson,
|
|
167
|
+
status: "ready",
|
|
168
|
+
priority,
|
|
169
|
+
run_at: runAt,
|
|
170
|
+
attempts: 0,
|
|
171
|
+
max_attempts: maxAttempts,
|
|
172
|
+
lease_until: 0,
|
|
173
|
+
created_at: timestamp,
|
|
174
|
+
updated_at: timestamp,
|
|
175
|
+
last_error: null
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
await trx.insertInto("workmatic_jobs").values(rows).execute();
|
|
179
|
+
return { ok: true, ids };
|
|
180
|
+
});
|
|
181
|
+
},
|
|
124
182
|
/**
|
|
125
183
|
* Get job statistics for the queue
|
|
126
184
|
*/
|
|
@@ -133,7 +191,6 @@ function createClient(options) {
|
|
|
133
191
|
ready: 0,
|
|
134
192
|
running: 0,
|
|
135
193
|
done: 0,
|
|
136
|
-
failed: 0,
|
|
137
194
|
dead: 0,
|
|
138
195
|
total: 0
|
|
139
196
|
};
|
|
@@ -164,6 +221,30 @@ function createClient(options) {
|
|
|
164
221
|
// src/worker.ts
|
|
165
222
|
import fastq from "fastq";
|
|
166
223
|
import { sql as sql2 } from "kysely";
|
|
224
|
+
var DEFAULT_WORKER_TIMEOUT_MS = 6e4;
|
|
225
|
+
function parseClaimedRows(result) {
|
|
226
|
+
const rows = result.rows;
|
|
227
|
+
if (!rows || !Array.isArray(rows)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
return rows.map((row) => ({
|
|
231
|
+
id: row.id,
|
|
232
|
+
public_id: row.public_id,
|
|
233
|
+
queue: row.queue,
|
|
234
|
+
payload: row.payload,
|
|
235
|
+
attempts: row.attempts,
|
|
236
|
+
max_attempts: row.max_attempts,
|
|
237
|
+
priority: row.priority,
|
|
238
|
+
created_at: row.created_at,
|
|
239
|
+
last_error: row.last_error
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
function requireProcessor(processor) {
|
|
243
|
+
if (!processor) {
|
|
244
|
+
throw new Error("No processor set");
|
|
245
|
+
}
|
|
246
|
+
return processor;
|
|
247
|
+
}
|
|
167
248
|
function createWorker(options) {
|
|
168
249
|
const {
|
|
169
250
|
db,
|
|
@@ -171,10 +252,13 @@ function createWorker(options) {
|
|
|
171
252
|
concurrency = 1,
|
|
172
253
|
leaseMs = 3e4,
|
|
173
254
|
pollMs = 1e3,
|
|
174
|
-
timeoutMs,
|
|
255
|
+
timeoutMs = DEFAULT_WORKER_TIMEOUT_MS,
|
|
175
256
|
backoff = defaultBackoff,
|
|
176
257
|
persistState = false,
|
|
177
|
-
autoRestore = true
|
|
258
|
+
autoRestore = true,
|
|
259
|
+
pauseCheckIntervalMs = 300,
|
|
260
|
+
requeueExpiredIntervalMs = 0,
|
|
261
|
+
onPumpError
|
|
178
262
|
} = options;
|
|
179
263
|
if (!db) {
|
|
180
264
|
throw new Error("Database instance is required");
|
|
@@ -184,6 +268,13 @@ function createWorker(options) {
|
|
|
184
268
|
let processor = null;
|
|
185
269
|
let pumpTimeout = null;
|
|
186
270
|
let fastqQueue = null;
|
|
271
|
+
let lastPauseCheckAt = 0;
|
|
272
|
+
let cachedDbPaused = false;
|
|
273
|
+
let lastRequeueAt = 0;
|
|
274
|
+
function notifyPumpError(error) {
|
|
275
|
+
console.error("[workmatic] Pump error:", error);
|
|
276
|
+
onPumpError?.(error);
|
|
277
|
+
}
|
|
187
278
|
function getStateKey() {
|
|
188
279
|
return `worker_state_${queue}`;
|
|
189
280
|
}
|
|
@@ -191,8 +282,6 @@ function createWorker(options) {
|
|
|
191
282
|
if (!persistState) return;
|
|
192
283
|
const timestamp = now();
|
|
193
284
|
const key = getStateKey();
|
|
194
|
-
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(() => {
|
|
195
|
-
});
|
|
196
285
|
await sql2`
|
|
197
286
|
INSERT INTO workmatic_settings (queue, paused, updated_at)
|
|
198
287
|
VALUES (${key}, ${state === "paused" ? 1 : state === "running" ? 2 : 0}, ${timestamp})
|
|
@@ -227,17 +316,20 @@ function createWorker(options) {
|
|
|
227
316
|
const timestamp = now();
|
|
228
317
|
const leaseUntil = timestamp + leaseMs;
|
|
229
318
|
return await db.transaction().execute(async (trx) => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
319
|
+
const result = await sql2`
|
|
320
|
+
UPDATE workmatic_jobs
|
|
321
|
+
SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
|
|
322
|
+
WHERE rowid IN (
|
|
323
|
+
SELECT rowid FROM workmatic_jobs
|
|
324
|
+
WHERE queue = ${queue}
|
|
325
|
+
AND status = 'ready'
|
|
326
|
+
AND run_at <= ${timestamp}
|
|
327
|
+
ORDER BY priority ASC, id ASC
|
|
328
|
+
LIMIT ${limit}
|
|
329
|
+
)
|
|
330
|
+
RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
|
|
331
|
+
`.execute(trx);
|
|
332
|
+
return parseClaimedRows(result);
|
|
241
333
|
});
|
|
242
334
|
}
|
|
243
335
|
async function markDone(jobId) {
|
|
@@ -285,28 +377,24 @@ function createWorker(options) {
|
|
|
285
377
|
}
|
|
286
378
|
}
|
|
287
379
|
async function processJob(claimedJob) {
|
|
288
|
-
|
|
289
|
-
throw new Error("No processor set");
|
|
290
|
-
}
|
|
380
|
+
const fn = requireProcessor(processor);
|
|
291
381
|
const payload = parsePayload(claimedJob.payload);
|
|
292
382
|
const job = {
|
|
293
383
|
id: claimedJob.public_id,
|
|
294
384
|
queue: claimedJob.queue,
|
|
295
385
|
payload,
|
|
296
386
|
status: "running",
|
|
297
|
-
priority:
|
|
298
|
-
// Not needed for processing
|
|
387
|
+
priority: claimedJob.priority,
|
|
299
388
|
attempts: claimedJob.attempts,
|
|
300
389
|
maxAttempts: claimedJob.max_attempts,
|
|
301
|
-
createdAt:
|
|
302
|
-
|
|
303
|
-
lastError: null
|
|
390
|
+
createdAt: claimedJob.created_at,
|
|
391
|
+
lastError: claimedJob.last_error
|
|
304
392
|
};
|
|
305
393
|
try {
|
|
306
394
|
if (timeoutMs) {
|
|
307
|
-
await withTimeout(
|
|
395
|
+
await withTimeout(fn(job), timeoutMs, job.id);
|
|
308
396
|
} else {
|
|
309
|
-
await
|
|
397
|
+
await fn(job);
|
|
310
398
|
}
|
|
311
399
|
await markDone(claimedJob.id);
|
|
312
400
|
} catch (error) {
|
|
@@ -331,12 +419,21 @@ function createWorker(options) {
|
|
|
331
419
|
return;
|
|
332
420
|
}
|
|
333
421
|
try {
|
|
334
|
-
const
|
|
335
|
-
if (
|
|
422
|
+
const t = now();
|
|
423
|
+
if (t - lastPauseCheckAt >= pauseCheckIntervalMs) {
|
|
424
|
+
lastPauseCheckAt = t;
|
|
425
|
+
cachedDbPaused = await isQueuePausedInDb();
|
|
426
|
+
}
|
|
427
|
+
if (cachedDbPaused) {
|
|
336
428
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
337
429
|
return;
|
|
338
430
|
}
|
|
339
|
-
|
|
431
|
+
if (requeueExpiredIntervalMs <= 0 || t - lastRequeueAt >= requeueExpiredIntervalMs) {
|
|
432
|
+
if (requeueExpiredIntervalMs > 0) {
|
|
433
|
+
lastRequeueAt = t;
|
|
434
|
+
}
|
|
435
|
+
await requeueExpiredLeases();
|
|
436
|
+
}
|
|
340
437
|
const batchSize = concurrency * 2;
|
|
341
438
|
const jobs = await claimBatch(batchSize);
|
|
342
439
|
if (jobs.length > 0) {
|
|
@@ -348,7 +445,7 @@ function createWorker(options) {
|
|
|
348
445
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
349
446
|
}
|
|
350
447
|
} catch (error) {
|
|
351
|
-
|
|
448
|
+
notifyPumpError(error);
|
|
352
449
|
pumpTimeout = setTimeout(pump, pollMs);
|
|
353
450
|
}
|
|
354
451
|
}
|
|
@@ -404,7 +501,6 @@ function createWorker(options) {
|
|
|
404
501
|
ready: 0,
|
|
405
502
|
running: 0,
|
|
406
503
|
done: 0,
|
|
407
|
-
failed: 0,
|
|
408
504
|
dead: 0,
|
|
409
505
|
total: 0
|
|
410
506
|
};
|
|
@@ -456,12 +552,211 @@ function createWorker(options) {
|
|
|
456
552
|
return worker;
|
|
457
553
|
}
|
|
458
554
|
|
|
555
|
+
// src/orchestrator.ts
|
|
556
|
+
import { sql as sql3 } from "kysely";
|
|
557
|
+
var DEFAULT_TRANSFER_STATUSES = ["ready", "dead"];
|
|
558
|
+
var DEFAULT_TRANSFER_LIMIT = 1e4;
|
|
559
|
+
function rowsUpdated(result) {
|
|
560
|
+
const row = result[0];
|
|
561
|
+
return Number(row?.numUpdatedRows ?? 0);
|
|
562
|
+
}
|
|
563
|
+
function normalizeStatuses(status) {
|
|
564
|
+
if (!status) return [...DEFAULT_TRANSFER_STATUSES];
|
|
565
|
+
return Array.isArray(status) ? status : [status];
|
|
566
|
+
}
|
|
567
|
+
function createOrchestrator(options) {
|
|
568
|
+
const { db } = options;
|
|
569
|
+
if (!db) {
|
|
570
|
+
throw new Error("Database instance is required");
|
|
571
|
+
}
|
|
572
|
+
const registry = /* @__PURE__ */ new Map();
|
|
573
|
+
function ensureEntry(queue) {
|
|
574
|
+
let entry = registry.get(queue);
|
|
575
|
+
if (!entry) {
|
|
576
|
+
entry = { client: createClient({ db, queue }) };
|
|
577
|
+
registry.set(queue, entry);
|
|
578
|
+
}
|
|
579
|
+
return entry;
|
|
580
|
+
}
|
|
581
|
+
async function setQueuePaused(queue, paused) {
|
|
582
|
+
const timestamp = now();
|
|
583
|
+
const value = paused ? 1 : 0;
|
|
584
|
+
await sql3`
|
|
585
|
+
INSERT INTO workmatic_settings (queue, paused, updated_at)
|
|
586
|
+
VALUES (${queue}, ${value}, ${timestamp})
|
|
587
|
+
ON CONFLICT(queue) DO UPDATE SET
|
|
588
|
+
paused = ${value},
|
|
589
|
+
updated_at = ${timestamp}
|
|
590
|
+
`.execute(db);
|
|
591
|
+
}
|
|
592
|
+
async function selectJobIds(from, status, limit) {
|
|
593
|
+
const rows = await db.selectFrom("workmatic_jobs").select("id").where("queue", "=", from).where("status", "=", status).orderBy("priority", "asc").orderBy("id", "asc").limit(limit).execute();
|
|
594
|
+
return rows.map((r) => r.id);
|
|
595
|
+
}
|
|
596
|
+
async function transferReady(from, to, limit) {
|
|
597
|
+
const ids = await selectJobIds(from, "ready", limit);
|
|
598
|
+
if (ids.length === 0) return 0;
|
|
599
|
+
const timestamp = now();
|
|
600
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
601
|
+
return rowsUpdated(result);
|
|
602
|
+
}
|
|
603
|
+
async function transferDead(from, to, limit, resetForRetry) {
|
|
604
|
+
const ids = await selectJobIds(from, "dead", limit);
|
|
605
|
+
if (ids.length === 0) return 0;
|
|
606
|
+
const timestamp = now();
|
|
607
|
+
if (resetForRetry) {
|
|
608
|
+
const result2 = await db.updateTable("workmatic_jobs").set({
|
|
609
|
+
queue: to,
|
|
610
|
+
status: "ready",
|
|
611
|
+
attempts: 0,
|
|
612
|
+
lease_until: 0,
|
|
613
|
+
last_error: null,
|
|
614
|
+
updated_at: timestamp,
|
|
615
|
+
run_at: timestamp
|
|
616
|
+
}).where("id", "in", ids).execute();
|
|
617
|
+
return rowsUpdated(result2);
|
|
618
|
+
}
|
|
619
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
620
|
+
return rowsUpdated(result);
|
|
621
|
+
}
|
|
622
|
+
async function transferOtherStatus(from, to, status, limit) {
|
|
623
|
+
const ids = await selectJobIds(from, status, limit);
|
|
624
|
+
if (ids.length === 0) return 0;
|
|
625
|
+
const timestamp = now();
|
|
626
|
+
const result = await db.updateTable("workmatic_jobs").set({ queue: to, updated_at: timestamp }).where("id", "in", ids).execute();
|
|
627
|
+
return rowsUpdated(result);
|
|
628
|
+
}
|
|
629
|
+
const orchestrator = {
|
|
630
|
+
register(queue, opts = {}) {
|
|
631
|
+
const client = createClient({ db, queue });
|
|
632
|
+
let worker;
|
|
633
|
+
if (opts.worker) {
|
|
634
|
+
worker = createWorker({ db, queue, ...opts.worker });
|
|
635
|
+
}
|
|
636
|
+
registry.set(queue, { client, worker });
|
|
637
|
+
return client;
|
|
638
|
+
},
|
|
639
|
+
client(queue) {
|
|
640
|
+
return ensureEntry(queue).client;
|
|
641
|
+
},
|
|
642
|
+
worker(queue) {
|
|
643
|
+
const entry = registry.get(queue);
|
|
644
|
+
if (!entry?.worker) {
|
|
645
|
+
throw new Error(`No worker registered for queue "${queue}"`);
|
|
646
|
+
}
|
|
647
|
+
return entry.worker;
|
|
648
|
+
},
|
|
649
|
+
workers() {
|
|
650
|
+
return [...registry.values()].map((e) => e.worker).filter((w) => w !== void 0);
|
|
651
|
+
},
|
|
652
|
+
async queues() {
|
|
653
|
+
const rows = await db.selectFrom("workmatic_jobs").select("queue").distinct().execute();
|
|
654
|
+
const names = new Set(rows.map((r) => r.queue));
|
|
655
|
+
for (const name of registry.keys()) {
|
|
656
|
+
names.add(name);
|
|
657
|
+
}
|
|
658
|
+
return [...names].sort();
|
|
659
|
+
},
|
|
660
|
+
process(queue, fn) {
|
|
661
|
+
this.worker(queue).process(fn);
|
|
662
|
+
},
|
|
663
|
+
startAll() {
|
|
664
|
+
for (const entry of registry.values()) {
|
|
665
|
+
entry.worker?.start();
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
async stopAll() {
|
|
669
|
+
const stops = [...registry.values()].map((e) => e.worker?.stop()).filter((p) => p !== void 0);
|
|
670
|
+
await Promise.all(stops);
|
|
671
|
+
},
|
|
672
|
+
async pause(queue) {
|
|
673
|
+
const entry = registry.get(queue);
|
|
674
|
+
entry?.worker?.pause();
|
|
675
|
+
await setQueuePaused(queue, true);
|
|
676
|
+
},
|
|
677
|
+
async resume(queue) {
|
|
678
|
+
const entry = registry.get(queue);
|
|
679
|
+
entry?.worker?.resume();
|
|
680
|
+
await setQueuePaused(queue, false);
|
|
681
|
+
},
|
|
682
|
+
async isPaused(queue) {
|
|
683
|
+
const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
|
|
684
|
+
return setting?.paused === 1;
|
|
685
|
+
},
|
|
686
|
+
async stats(queueName) {
|
|
687
|
+
const names = queueName ? [queueName] : await this.queues();
|
|
688
|
+
const result = {};
|
|
689
|
+
for (const name of names) {
|
|
690
|
+
result[name] = await ensureEntry(name).client.stats();
|
|
691
|
+
}
|
|
692
|
+
return result;
|
|
693
|
+
},
|
|
694
|
+
async transfer(opts) {
|
|
695
|
+
const { from, to, resetForRetry = false } = opts;
|
|
696
|
+
if (from === to) {
|
|
697
|
+
return { moved: 0 };
|
|
698
|
+
}
|
|
699
|
+
const statuses = normalizeStatuses(opts.status);
|
|
700
|
+
let remaining = opts.limit ?? DEFAULT_TRANSFER_LIMIT;
|
|
701
|
+
let moved = 0;
|
|
702
|
+
if (statuses.includes("ready") && remaining > 0) {
|
|
703
|
+
const n = await transferReady(from, to, remaining);
|
|
704
|
+
moved += n;
|
|
705
|
+
remaining -= n;
|
|
706
|
+
}
|
|
707
|
+
if (statuses.includes("dead") && remaining > 0) {
|
|
708
|
+
const n = await transferDead(from, to, remaining, resetForRetry);
|
|
709
|
+
moved += n;
|
|
710
|
+
remaining -= n;
|
|
711
|
+
}
|
|
712
|
+
for (const status of statuses) {
|
|
713
|
+
if (status === "ready" || status === "dead" || remaining <= 0) continue;
|
|
714
|
+
const n = await transferOtherStatus(from, to, status, remaining);
|
|
715
|
+
moved += n;
|
|
716
|
+
remaining -= n;
|
|
717
|
+
}
|
|
718
|
+
return { moved };
|
|
719
|
+
},
|
|
720
|
+
async moveJob(publicId, toQueue, opts = {}) {
|
|
721
|
+
const allowed = normalizeStatuses(opts.status);
|
|
722
|
+
const row = await db.selectFrom("workmatic_jobs").select(["id", "queue", "status"]).where("public_id", "=", publicId).executeTakeFirst();
|
|
723
|
+
if (!row) {
|
|
724
|
+
throw new Error(`Job not found: ${publicId}`);
|
|
725
|
+
}
|
|
726
|
+
if (row.queue === toQueue) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (!allowed.includes(row.status)) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`Job ${publicId} has status "${row.status}" and cannot be moved (allowed: ${allowed.join(", ")})`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const timestamp = now();
|
|
735
|
+
const resetForRetry = opts.resetForRetry ?? false;
|
|
736
|
+
if (row.status === "dead" && resetForRetry) {
|
|
737
|
+
await db.updateTable("workmatic_jobs").set({
|
|
738
|
+
queue: toQueue,
|
|
739
|
+
status: "ready",
|
|
740
|
+
attempts: 0,
|
|
741
|
+
lease_until: 0,
|
|
742
|
+
last_error: null,
|
|
743
|
+
updated_at: timestamp,
|
|
744
|
+
run_at: timestamp
|
|
745
|
+
}).where("public_id", "=", publicId).execute();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
await db.updateTable("workmatic_jobs").set({ queue: toQueue, updated_at: timestamp }).where("public_id", "=", publicId).execute();
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
return orchestrator;
|
|
752
|
+
}
|
|
753
|
+
|
|
459
754
|
// src/dashboard.ts
|
|
460
755
|
import { createServer } from "http";
|
|
461
756
|
import { fileURLToPath } from "url";
|
|
462
757
|
import { dirname, join } from "path";
|
|
463
758
|
import { readFile } from "fs/promises";
|
|
464
|
-
import { sql as
|
|
759
|
+
import { sql as sql4 } from "kysely";
|
|
465
760
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
466
761
|
var __dirname2 = dirname(__filename2);
|
|
467
762
|
var CONTENT_TYPES = {
|
|
@@ -470,6 +765,13 @@ var CONTENT_TYPES = {
|
|
|
470
765
|
".js": "application/javascript; charset=utf-8",
|
|
471
766
|
".json": "application/json; charset=utf-8"
|
|
472
767
|
};
|
|
768
|
+
function requestUrl(url) {
|
|
769
|
+
return url || "";
|
|
770
|
+
}
|
|
771
|
+
function staticContentTypeFor(filePath) {
|
|
772
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")) || ".html";
|
|
773
|
+
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
774
|
+
}
|
|
473
775
|
function createRequestHandler(db, workerMap, basePath = "") {
|
|
474
776
|
function sendJson(res, data, status = 200) {
|
|
475
777
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -492,7 +794,7 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
492
794
|
return path;
|
|
493
795
|
}
|
|
494
796
|
async function handleGetJobs(req, res) {
|
|
495
|
-
const query = parseQuery(req.url
|
|
797
|
+
const query = parseQuery(requestUrl(req.url));
|
|
496
798
|
const queueFilter = query.get("queue");
|
|
497
799
|
const statusFilter = query.get("status");
|
|
498
800
|
const limit = Math.min(parseInt(query.get("limit") || "50", 10), 100);
|
|
@@ -533,11 +835,11 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
533
835
|
sendJson(res, { jobs: apiJobs, limit, offset });
|
|
534
836
|
}
|
|
535
837
|
async function handleGetStats(req, res) {
|
|
536
|
-
const query = parseQuery(req.url
|
|
838
|
+
const query = parseQuery(requestUrl(req.url));
|
|
537
839
|
const queueFilter = query.get("queue");
|
|
538
840
|
let statsQuery = db.selectFrom("workmatic_jobs").select([
|
|
539
841
|
"status",
|
|
540
|
-
|
|
842
|
+
sql4`count(*)`.as("count")
|
|
541
843
|
]).groupBy("status");
|
|
542
844
|
if (queueFilter) {
|
|
543
845
|
statsQuery = statsQuery.where("queue", "=", queueFilter);
|
|
@@ -547,7 +849,6 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
547
849
|
ready: 0,
|
|
548
850
|
running: 0,
|
|
549
851
|
done: 0,
|
|
550
|
-
failed: 0,
|
|
551
852
|
dead: 0,
|
|
552
853
|
total: 0
|
|
553
854
|
};
|
|
@@ -620,8 +921,7 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
620
921
|
async function serveStatic(res, filePath) {
|
|
621
922
|
const dashboardDir = join(__dirname2, "..", "dashboard");
|
|
622
923
|
const fullPath = join(dashboardDir, filePath);
|
|
623
|
-
const
|
|
624
|
-
const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
|
|
924
|
+
const contentType = staticContentTypeFor(filePath);
|
|
625
925
|
try {
|
|
626
926
|
const content = await readFile(fullPath, "utf-8");
|
|
627
927
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -631,21 +931,21 @@ function createRequestHandler(db, workerMap, basePath = "") {
|
|
|
631
931
|
}
|
|
632
932
|
}
|
|
633
933
|
return async function handleRequest(req, res, next) {
|
|
634
|
-
const path = getPath(req.url || "/");
|
|
635
|
-
const fullUrl = req.url || "/";
|
|
636
|
-
if (basePath && !fullUrl.startsWith(basePath)) {
|
|
637
|
-
if (next) next();
|
|
638
|
-
return false;
|
|
639
|
-
}
|
|
640
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
641
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
642
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
643
|
-
if (req.method === "OPTIONS") {
|
|
644
|
-
res.writeHead(204);
|
|
645
|
-
res.end();
|
|
646
|
-
return true;
|
|
647
|
-
}
|
|
648
934
|
try {
|
|
935
|
+
const path = getPath(req.url || "/");
|
|
936
|
+
const fullUrl = req.url || "/";
|
|
937
|
+
if (basePath && !fullUrl.startsWith(basePath)) {
|
|
938
|
+
if (next) next();
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
942
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
943
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
944
|
+
if (req.method === "OPTIONS") {
|
|
945
|
+
res.writeHead(204);
|
|
946
|
+
res.end();
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
649
949
|
if (path === "/api/jobs" && req.method === "GET") {
|
|
650
950
|
await handleGetJobs(req, res);
|
|
651
951
|
return true;
|
|
@@ -705,11 +1005,7 @@ function createDashboard(options) {
|
|
|
705
1005
|
}
|
|
706
1006
|
const handleRequest = createRequestHandler(db, workerMap);
|
|
707
1007
|
const server = createServer((req, res) => {
|
|
708
|
-
handleRequest(req, res)
|
|
709
|
-
console.error("[workmatic] Unhandled error:", error);
|
|
710
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
711
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
712
|
-
});
|
|
1008
|
+
void handleRequest(req, res);
|
|
713
1009
|
});
|
|
714
1010
|
server.listen(port);
|
|
715
1011
|
return {
|
|
@@ -741,24 +1037,19 @@ function createDashboardMiddleware(options) {
|
|
|
741
1037
|
}
|
|
742
1038
|
const handleRequest = createRequestHandler(db, workerMap, basePath);
|
|
743
1039
|
return (req, res, next) => {
|
|
744
|
-
handleRequest(req, res, next)
|
|
745
|
-
console.error("[workmatic] Unhandled error:", error);
|
|
746
|
-
if (next) {
|
|
747
|
-
next();
|
|
748
|
-
} else {
|
|
749
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
750
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
751
|
-
}
|
|
752
|
-
});
|
|
1040
|
+
void handleRequest(req, res, next);
|
|
753
1041
|
};
|
|
754
1042
|
}
|
|
755
1043
|
export {
|
|
1044
|
+
DEFAULT_WORKER_TIMEOUT_MS,
|
|
756
1045
|
createClient,
|
|
757
1046
|
createDashboard,
|
|
758
1047
|
createDashboardMiddleware,
|
|
759
1048
|
createDatabase,
|
|
1049
|
+
createOrchestrator,
|
|
760
1050
|
createWorker,
|
|
761
1051
|
defaultBackoff,
|
|
1052
|
+
getUnderlyingDb,
|
|
762
1053
|
validatePayload
|
|
763
1054
|
};
|
|
764
1055
|
//# sourceMappingURL=index.js.map
|