workmatic 1.0.0 → 1.0.2
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/dist/cli.cjs +559 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +0 -14
- package/dist/cli.js +400 -369
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +736 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +368 -0
- package/dist/index.d.ts +365 -9
- package/dist/index.js +688 -12
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
- package/dist/cli.d.ts.map +0 -1
- package/dist/client.d.ts +0 -28
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -99
- package/dist/client.js.map +0 -1
- package/dist/dashboard.d.ts +0 -48
- package/dist/dashboard.d.ts.map +0 -1
- package/dist/dashboard.js +0 -412
- package/dist/dashboard.js.map +0 -1
- package/dist/database.d.ts +0 -29
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js +0 -99
- package/dist/database.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -218
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts +0 -45
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -64
- package/dist/utils.js.map +0 -1
- package/dist/worker.d.ts +0 -26
- package/dist/worker.d.ts.map +0 -1
- package/dist/worker.js +0 -331
- package/dist/worker.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createClient: () => createClient,
|
|
34
|
+
createDashboard: () => createDashboard,
|
|
35
|
+
createDashboardMiddleware: () => createDashboardMiddleware,
|
|
36
|
+
createDatabase: () => createDatabase,
|
|
37
|
+
createWorker: () => createWorker,
|
|
38
|
+
defaultBackoff: () => defaultBackoff,
|
|
39
|
+
validatePayload: () => validatePayload
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
44
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
45
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
46
|
+
|
|
47
|
+
// src/database.ts
|
|
48
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
49
|
+
var import_kysely = require("kysely");
|
|
50
|
+
function createDatabase(options = {}) {
|
|
51
|
+
let sqliteDb;
|
|
52
|
+
if (options.db) {
|
|
53
|
+
sqliteDb = options.db;
|
|
54
|
+
} else {
|
|
55
|
+
const filename = options.filename ?? ":memory:";
|
|
56
|
+
sqliteDb = new import_better_sqlite3.default(filename);
|
|
57
|
+
}
|
|
58
|
+
sqliteDb.pragma("journal_mode = WAL");
|
|
59
|
+
sqliteDb.pragma("synchronous = NORMAL");
|
|
60
|
+
sqliteDb.pragma("busy_timeout = 5000");
|
|
61
|
+
const db = new import_kysely.Kysely({
|
|
62
|
+
dialect: new import_kysely.SqliteDialect({
|
|
63
|
+
database: sqliteDb
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
createSchema(sqliteDb);
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
function createSchema(db) {
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS workmatic_jobs (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
public_id TEXT UNIQUE NOT NULL,
|
|
74
|
+
queue TEXT NOT NULL,
|
|
75
|
+
payload TEXT NOT NULL,
|
|
76
|
+
status TEXT NOT NULL DEFAULT 'ready',
|
|
77
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
run_at INTEGER NOT NULL,
|
|
79
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
81
|
+
lease_until INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
created_at INTEGER NOT NULL,
|
|
83
|
+
updated_at INTEGER NOT NULL,
|
|
84
|
+
last_error TEXT
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
db.exec(`
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_claim
|
|
89
|
+
ON workmatic_jobs (queue, status, run_at, priority, id)
|
|
90
|
+
`);
|
|
91
|
+
db.exec(`
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_workmatic_jobs_lease
|
|
93
|
+
ON workmatic_jobs (status, lease_until)
|
|
94
|
+
`);
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS workmatic_settings (
|
|
97
|
+
queue TEXT PRIMARY KEY,
|
|
98
|
+
paused INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
updated_at INTEGER NOT NULL
|
|
100
|
+
)
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/client.ts
|
|
105
|
+
var import_nanoid = require("nanoid");
|
|
106
|
+
var import_kysely2 = require("kysely");
|
|
107
|
+
|
|
108
|
+
// src/utils.ts
|
|
109
|
+
var defaultBackoff = (attempts) => {
|
|
110
|
+
return 1e3 * Math.pow(2, attempts);
|
|
111
|
+
};
|
|
112
|
+
function validatePayload(payload) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.stringify(payload);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Payload is not JSON-serializable: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function parsePayload(json) {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(json);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Invalid JSON payload: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function now() {
|
|
131
|
+
return Date.now();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/client.ts
|
|
135
|
+
function createClient(options) {
|
|
136
|
+
const { db, queue = "default" } = options;
|
|
137
|
+
if (!db) {
|
|
138
|
+
throw new Error("Database instance is required");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
/**
|
|
142
|
+
* Add a job to the queue
|
|
143
|
+
*/
|
|
144
|
+
async add(payload, opts = {}) {
|
|
145
|
+
const {
|
|
146
|
+
priority = 0,
|
|
147
|
+
delayMs = 0,
|
|
148
|
+
maxAttempts = 3
|
|
149
|
+
} = opts;
|
|
150
|
+
const payloadJson = validatePayload(payload);
|
|
151
|
+
const publicId = (0, import_nanoid.nanoid)();
|
|
152
|
+
const timestamp = now();
|
|
153
|
+
const runAt = timestamp + delayMs;
|
|
154
|
+
await db.insertInto("workmatic_jobs").values({
|
|
155
|
+
public_id: publicId,
|
|
156
|
+
queue,
|
|
157
|
+
payload: payloadJson,
|
|
158
|
+
status: "ready",
|
|
159
|
+
priority,
|
|
160
|
+
run_at: runAt,
|
|
161
|
+
attempts: 0,
|
|
162
|
+
max_attempts: maxAttempts,
|
|
163
|
+
lease_until: 0,
|
|
164
|
+
created_at: timestamp,
|
|
165
|
+
updated_at: timestamp,
|
|
166
|
+
last_error: null
|
|
167
|
+
}).execute();
|
|
168
|
+
return { ok: true, id: publicId };
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* Get job statistics for the queue
|
|
172
|
+
*/
|
|
173
|
+
async stats() {
|
|
174
|
+
const result = await db.selectFrom("workmatic_jobs").select([
|
|
175
|
+
"status",
|
|
176
|
+
import_kysely2.sql`count(*)`.as("count")
|
|
177
|
+
]).where("queue", "=", queue).groupBy("status").execute();
|
|
178
|
+
const stats = {
|
|
179
|
+
ready: 0,
|
|
180
|
+
running: 0,
|
|
181
|
+
done: 0,
|
|
182
|
+
failed: 0,
|
|
183
|
+
dead: 0,
|
|
184
|
+
total: 0
|
|
185
|
+
};
|
|
186
|
+
for (const row of result) {
|
|
187
|
+
const status = row.status;
|
|
188
|
+
const count = Number(row.count);
|
|
189
|
+
if (status in stats) {
|
|
190
|
+
stats[status] = count;
|
|
191
|
+
}
|
|
192
|
+
stats.total += count;
|
|
193
|
+
}
|
|
194
|
+
return stats;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/worker.ts
|
|
200
|
+
var import_fastq = __toESM(require("fastq"), 1);
|
|
201
|
+
var import_kysely3 = require("kysely");
|
|
202
|
+
function createWorker(options) {
|
|
203
|
+
const {
|
|
204
|
+
db,
|
|
205
|
+
queue = "default",
|
|
206
|
+
concurrency = 1,
|
|
207
|
+
leaseMs = 3e4,
|
|
208
|
+
pollMs = 1e3,
|
|
209
|
+
timeoutMs,
|
|
210
|
+
backoff = defaultBackoff
|
|
211
|
+
} = options;
|
|
212
|
+
if (!db) {
|
|
213
|
+
throw new Error("Database instance is required");
|
|
214
|
+
}
|
|
215
|
+
let running = false;
|
|
216
|
+
let paused = false;
|
|
217
|
+
let processor = null;
|
|
218
|
+
let pumpTimeout = null;
|
|
219
|
+
let fastqQueue = null;
|
|
220
|
+
async function requeueExpiredLeases() {
|
|
221
|
+
const timestamp = now();
|
|
222
|
+
const result = await db.updateTable("workmatic_jobs").set({
|
|
223
|
+
status: "ready",
|
|
224
|
+
lease_until: 0,
|
|
225
|
+
updated_at: timestamp
|
|
226
|
+
}).where("status", "=", "running").where("lease_until", "<", timestamp).where("lease_until", ">", 0).execute();
|
|
227
|
+
return Number(result[0]?.numUpdatedRows ?? 0);
|
|
228
|
+
}
|
|
229
|
+
async function claimBatch(limit) {
|
|
230
|
+
const timestamp = now();
|
|
231
|
+
const leaseUntil = timestamp + leaseMs;
|
|
232
|
+
return await db.transaction().execute(async (trx) => {
|
|
233
|
+
const jobs = await trx.selectFrom("workmatic_jobs").select(["id", "public_id", "queue", "payload", "attempts", "max_attempts"]).where("queue", "=", queue).where("status", "=", "ready").where("run_at", "<=", timestamp).orderBy("priority", "asc").orderBy("id", "asc").limit(limit).execute();
|
|
234
|
+
if (jobs.length === 0) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const jobIds = jobs.map((j) => j.id);
|
|
238
|
+
await trx.updateTable("workmatic_jobs").set({
|
|
239
|
+
status: "running",
|
|
240
|
+
lease_until: leaseUntil,
|
|
241
|
+
updated_at: timestamp
|
|
242
|
+
}).where("id", "in", jobIds).execute();
|
|
243
|
+
return jobs;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function markDone(jobId) {
|
|
247
|
+
await db.updateTable("workmatic_jobs").set({
|
|
248
|
+
status: "done",
|
|
249
|
+
lease_until: 0,
|
|
250
|
+
updated_at: now()
|
|
251
|
+
}).where("id", "=", jobId).execute();
|
|
252
|
+
}
|
|
253
|
+
async function markFailed(jobId, attempts, maxAttempts, error) {
|
|
254
|
+
const timestamp = now();
|
|
255
|
+
const newAttempts = attempts + 1;
|
|
256
|
+
const errorMessage = error.message || String(error);
|
|
257
|
+
if (newAttempts < maxAttempts) {
|
|
258
|
+
const runAt = timestamp + backoff(newAttempts);
|
|
259
|
+
await db.updateTable("workmatic_jobs").set({
|
|
260
|
+
status: "ready",
|
|
261
|
+
attempts: newAttempts,
|
|
262
|
+
run_at: runAt,
|
|
263
|
+
lease_until: 0,
|
|
264
|
+
last_error: errorMessage,
|
|
265
|
+
updated_at: timestamp
|
|
266
|
+
}).where("id", "=", jobId).execute();
|
|
267
|
+
} else {
|
|
268
|
+
await db.updateTable("workmatic_jobs").set({
|
|
269
|
+
status: "dead",
|
|
270
|
+
attempts: newAttempts,
|
|
271
|
+
lease_until: 0,
|
|
272
|
+
last_error: errorMessage,
|
|
273
|
+
updated_at: timestamp
|
|
274
|
+
}).where("id", "=", jobId).execute();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function withTimeout(promise, ms, jobId) {
|
|
278
|
+
let timeoutId;
|
|
279
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
280
|
+
timeoutId = setTimeout(() => {
|
|
281
|
+
reject(new Error(`Job ${jobId} timed out after ${ms}ms`));
|
|
282
|
+
}, ms);
|
|
283
|
+
});
|
|
284
|
+
try {
|
|
285
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
286
|
+
} finally {
|
|
287
|
+
clearTimeout(timeoutId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function processJob(claimedJob) {
|
|
291
|
+
if (!processor) {
|
|
292
|
+
throw new Error("No processor set");
|
|
293
|
+
}
|
|
294
|
+
const payload = parsePayload(claimedJob.payload);
|
|
295
|
+
const job = {
|
|
296
|
+
id: claimedJob.public_id,
|
|
297
|
+
queue: claimedJob.queue,
|
|
298
|
+
payload,
|
|
299
|
+
status: "running",
|
|
300
|
+
priority: 0,
|
|
301
|
+
// Not needed for processing
|
|
302
|
+
attempts: claimedJob.attempts,
|
|
303
|
+
maxAttempts: claimedJob.max_attempts,
|
|
304
|
+
createdAt: 0,
|
|
305
|
+
// Not needed for processing
|
|
306
|
+
lastError: null
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
if (timeoutMs) {
|
|
310
|
+
await withTimeout(processor(job), timeoutMs, job.id);
|
|
311
|
+
} else {
|
|
312
|
+
await processor(job);
|
|
313
|
+
}
|
|
314
|
+
await markDone(claimedJob.id);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
await markFailed(
|
|
317
|
+
claimedJob.id,
|
|
318
|
+
claimedJob.attempts,
|
|
319
|
+
claimedJob.max_attempts,
|
|
320
|
+
error instanceof Error ? error : new Error(String(error))
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function isQueuePausedInDb() {
|
|
325
|
+
const setting = await db.selectFrom("workmatic_settings").select("paused").where("queue", "=", queue).executeTakeFirst();
|
|
326
|
+
return setting?.paused === 1;
|
|
327
|
+
}
|
|
328
|
+
async function pump() {
|
|
329
|
+
if (!running) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (paused) {
|
|
333
|
+
pumpTimeout = setTimeout(pump, pollMs);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const dbPaused = await isQueuePausedInDb();
|
|
338
|
+
if (dbPaused) {
|
|
339
|
+
pumpTimeout = setTimeout(pump, pollMs);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
await requeueExpiredLeases();
|
|
343
|
+
const batchSize = concurrency * 2;
|
|
344
|
+
const jobs = await claimBatch(batchSize);
|
|
345
|
+
if (jobs.length > 0) {
|
|
346
|
+
for (const job of jobs) {
|
|
347
|
+
fastqQueue.push(job);
|
|
348
|
+
}
|
|
349
|
+
pumpTimeout = setTimeout(pump, 0);
|
|
350
|
+
} else {
|
|
351
|
+
pumpTimeout = setTimeout(pump, pollMs);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error("[workmatic] Pump error:", error);
|
|
355
|
+
pumpTimeout = setTimeout(pump, pollMs);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const worker = {
|
|
359
|
+
process(fn) {
|
|
360
|
+
processor = fn;
|
|
361
|
+
},
|
|
362
|
+
start() {
|
|
363
|
+
if (running) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!processor) {
|
|
367
|
+
throw new Error("No processor set. Call process() before start()");
|
|
368
|
+
}
|
|
369
|
+
running = true;
|
|
370
|
+
paused = false;
|
|
371
|
+
fastqQueue = import_fastq.default.promise(processJob, concurrency);
|
|
372
|
+
pump();
|
|
373
|
+
},
|
|
374
|
+
async stop() {
|
|
375
|
+
if (!running) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
running = false;
|
|
379
|
+
if (pumpTimeout) {
|
|
380
|
+
clearTimeout(pumpTimeout);
|
|
381
|
+
pumpTimeout = null;
|
|
382
|
+
}
|
|
383
|
+
if (fastqQueue) {
|
|
384
|
+
await fastqQueue.drained();
|
|
385
|
+
fastqQueue = null;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
pause() {
|
|
389
|
+
paused = true;
|
|
390
|
+
},
|
|
391
|
+
resume() {
|
|
392
|
+
paused = false;
|
|
393
|
+
},
|
|
394
|
+
async stats() {
|
|
395
|
+
const result = await db.selectFrom("workmatic_jobs").select([
|
|
396
|
+
"status",
|
|
397
|
+
import_kysely3.sql`count(*)`.as("count")
|
|
398
|
+
]).where("queue", "=", queue).groupBy("status").execute();
|
|
399
|
+
const stats = {
|
|
400
|
+
ready: 0,
|
|
401
|
+
running: 0,
|
|
402
|
+
done: 0,
|
|
403
|
+
failed: 0,
|
|
404
|
+
dead: 0,
|
|
405
|
+
total: 0
|
|
406
|
+
};
|
|
407
|
+
for (const row of result) {
|
|
408
|
+
const status = row.status;
|
|
409
|
+
const count = Number(row.count);
|
|
410
|
+
if (status in stats) {
|
|
411
|
+
stats[status] = count;
|
|
412
|
+
}
|
|
413
|
+
stats.total += count;
|
|
414
|
+
}
|
|
415
|
+
return stats;
|
|
416
|
+
},
|
|
417
|
+
get isRunning() {
|
|
418
|
+
return running;
|
|
419
|
+
},
|
|
420
|
+
get isPaused() {
|
|
421
|
+
return paused;
|
|
422
|
+
},
|
|
423
|
+
get queue() {
|
|
424
|
+
return queue;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
return worker;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/dashboard.ts
|
|
431
|
+
var import_http = require("http");
|
|
432
|
+
var import_url = require("url");
|
|
433
|
+
var import_path = require("path");
|
|
434
|
+
var import_promises = require("fs/promises");
|
|
435
|
+
var import_kysely4 = require("kysely");
|
|
436
|
+
var __filename2 = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
437
|
+
var __dirname = (0, import_path.dirname)(__filename2);
|
|
438
|
+
var CONTENT_TYPES = {
|
|
439
|
+
".html": "text/html; charset=utf-8",
|
|
440
|
+
".css": "text/css; charset=utf-8",
|
|
441
|
+
".js": "application/javascript; charset=utf-8",
|
|
442
|
+
".json": "application/json; charset=utf-8"
|
|
443
|
+
};
|
|
444
|
+
function createRequestHandler(db, workerMap, basePath = "") {
|
|
445
|
+
function sendJson(res, data, status = 200) {
|
|
446
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
447
|
+
res.end(JSON.stringify(data));
|
|
448
|
+
}
|
|
449
|
+
function sendError(res, message, status = 500) {
|
|
450
|
+
sendJson(res, { error: message }, status);
|
|
451
|
+
}
|
|
452
|
+
function parseQuery(url) {
|
|
453
|
+
const queryIndex = url.indexOf("?");
|
|
454
|
+
if (queryIndex === -1) return new URLSearchParams();
|
|
455
|
+
return new URLSearchParams(url.slice(queryIndex + 1));
|
|
456
|
+
}
|
|
457
|
+
function getPath(url) {
|
|
458
|
+
const queryIndex = url.indexOf("?");
|
|
459
|
+
let path = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
|
460
|
+
if (basePath && path.startsWith(basePath)) {
|
|
461
|
+
path = path.slice(basePath.length) || "/";
|
|
462
|
+
}
|
|
463
|
+
return path;
|
|
464
|
+
}
|
|
465
|
+
async function handleGetJobs(req, res) {
|
|
466
|
+
const query = parseQuery(req.url || "");
|
|
467
|
+
const queueFilter = query.get("queue");
|
|
468
|
+
const statusFilter = query.get("status");
|
|
469
|
+
const limit = Math.min(parseInt(query.get("limit") || "50", 10), 100);
|
|
470
|
+
const offset = parseInt(query.get("offset") || "0", 10);
|
|
471
|
+
let queryBuilder = db.selectFrom("workmatic_jobs").select([
|
|
472
|
+
"public_id",
|
|
473
|
+
"queue",
|
|
474
|
+
"payload",
|
|
475
|
+
"status",
|
|
476
|
+
"priority",
|
|
477
|
+
"attempts",
|
|
478
|
+
"max_attempts",
|
|
479
|
+
"run_at",
|
|
480
|
+
"created_at",
|
|
481
|
+
"updated_at",
|
|
482
|
+
"last_error"
|
|
483
|
+
]).orderBy("created_at", "desc").limit(limit).offset(offset);
|
|
484
|
+
if (queueFilter) {
|
|
485
|
+
queryBuilder = queryBuilder.where("queue", "=", queueFilter);
|
|
486
|
+
}
|
|
487
|
+
if (statusFilter) {
|
|
488
|
+
queryBuilder = queryBuilder.where("status", "=", statusFilter);
|
|
489
|
+
}
|
|
490
|
+
const jobs = await queryBuilder.execute();
|
|
491
|
+
const apiJobs = jobs.map((job) => ({
|
|
492
|
+
id: job.public_id,
|
|
493
|
+
queue: job.queue,
|
|
494
|
+
payload: JSON.parse(job.payload),
|
|
495
|
+
status: job.status,
|
|
496
|
+
priority: job.priority,
|
|
497
|
+
attempts: job.attempts,
|
|
498
|
+
maxAttempts: job.max_attempts,
|
|
499
|
+
runAt: job.run_at,
|
|
500
|
+
createdAt: job.created_at,
|
|
501
|
+
updatedAt: job.updated_at,
|
|
502
|
+
lastError: job.last_error
|
|
503
|
+
}));
|
|
504
|
+
sendJson(res, { jobs: apiJobs, limit, offset });
|
|
505
|
+
}
|
|
506
|
+
async function handleGetStats(req, res) {
|
|
507
|
+
const query = parseQuery(req.url || "");
|
|
508
|
+
const queueFilter = query.get("queue");
|
|
509
|
+
let statsQuery = db.selectFrom("workmatic_jobs").select([
|
|
510
|
+
"status",
|
|
511
|
+
import_kysely4.sql`count(*)`.as("count")
|
|
512
|
+
]).groupBy("status");
|
|
513
|
+
if (queueFilter) {
|
|
514
|
+
statsQuery = statsQuery.where("queue", "=", queueFilter);
|
|
515
|
+
}
|
|
516
|
+
const statusCounts = await statsQuery.execute();
|
|
517
|
+
const stats = {
|
|
518
|
+
ready: 0,
|
|
519
|
+
running: 0,
|
|
520
|
+
done: 0,
|
|
521
|
+
failed: 0,
|
|
522
|
+
dead: 0,
|
|
523
|
+
total: 0
|
|
524
|
+
};
|
|
525
|
+
for (const row of statusCounts) {
|
|
526
|
+
const status = row.status;
|
|
527
|
+
const count = Number(row.count);
|
|
528
|
+
if (status in stats) {
|
|
529
|
+
stats[status] = count;
|
|
530
|
+
}
|
|
531
|
+
stats.total += count;
|
|
532
|
+
}
|
|
533
|
+
const queuesResult = await db.selectFrom("workmatic_jobs").select("queue").groupBy("queue").execute();
|
|
534
|
+
const queues = queuesResult.map((q) => q.queue);
|
|
535
|
+
const workersStatus = Array.from(workerMap.entries()).map(([queue, worker]) => ({
|
|
536
|
+
queue,
|
|
537
|
+
running: worker.isRunning,
|
|
538
|
+
paused: worker.isPaused
|
|
539
|
+
}));
|
|
540
|
+
sendJson(res, { stats, queues, workers: workersStatus });
|
|
541
|
+
}
|
|
542
|
+
async function handleGetJob(req, res, publicId) {
|
|
543
|
+
const job = await db.selectFrom("workmatic_jobs").select([
|
|
544
|
+
"public_id",
|
|
545
|
+
"queue",
|
|
546
|
+
"payload",
|
|
547
|
+
"status",
|
|
548
|
+
"priority",
|
|
549
|
+
"attempts",
|
|
550
|
+
"max_attempts",
|
|
551
|
+
"run_at",
|
|
552
|
+
"lease_until",
|
|
553
|
+
"created_at",
|
|
554
|
+
"updated_at",
|
|
555
|
+
"last_error"
|
|
556
|
+
]).where("public_id", "=", publicId).executeTakeFirst();
|
|
557
|
+
if (!job) {
|
|
558
|
+
return sendError(res, "Job not found", 404);
|
|
559
|
+
}
|
|
560
|
+
sendJson(res, {
|
|
561
|
+
id: job.public_id,
|
|
562
|
+
queue: job.queue,
|
|
563
|
+
payload: JSON.parse(job.payload),
|
|
564
|
+
status: job.status,
|
|
565
|
+
priority: job.priority,
|
|
566
|
+
attempts: job.attempts,
|
|
567
|
+
maxAttempts: job.max_attempts,
|
|
568
|
+
runAt: job.run_at,
|
|
569
|
+
leaseUntil: job.lease_until,
|
|
570
|
+
createdAt: job.created_at,
|
|
571
|
+
updatedAt: job.updated_at,
|
|
572
|
+
lastError: job.last_error
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async function handlePauseWorker(res, queue) {
|
|
576
|
+
const worker = workerMap.get(queue);
|
|
577
|
+
if (!worker) {
|
|
578
|
+
return sendError(res, `Worker for queue '${queue}' not found`, 404);
|
|
579
|
+
}
|
|
580
|
+
worker.pause();
|
|
581
|
+
sendJson(res, { ok: true, queue, paused: true });
|
|
582
|
+
}
|
|
583
|
+
async function handleResumeWorker(res, queue) {
|
|
584
|
+
const worker = workerMap.get(queue);
|
|
585
|
+
if (!worker) {
|
|
586
|
+
return sendError(res, `Worker for queue '${queue}' not found`, 404);
|
|
587
|
+
}
|
|
588
|
+
worker.resume();
|
|
589
|
+
sendJson(res, { ok: true, queue, paused: false });
|
|
590
|
+
}
|
|
591
|
+
async function serveStatic(res, filePath) {
|
|
592
|
+
const dashboardDir = (0, import_path.join)(__dirname, "..", "dashboard");
|
|
593
|
+
const fullPath = (0, import_path.join)(dashboardDir, filePath);
|
|
594
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")) || ".html";
|
|
595
|
+
const contentType = CONTENT_TYPES[ext] || "application/octet-stream";
|
|
596
|
+
try {
|
|
597
|
+
const content = await (0, import_promises.readFile)(fullPath, "utf-8");
|
|
598
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
599
|
+
res.end(content);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
sendError(res, "Not found", 404);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return async function handleRequest(req, res, next) {
|
|
605
|
+
const path = getPath(req.url || "/");
|
|
606
|
+
const fullUrl = req.url || "/";
|
|
607
|
+
if (basePath && !fullUrl.startsWith(basePath)) {
|
|
608
|
+
if (next) next();
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
612
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
613
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
614
|
+
if (req.method === "OPTIONS") {
|
|
615
|
+
res.writeHead(204);
|
|
616
|
+
res.end();
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
if (path === "/api/jobs" && req.method === "GET") {
|
|
621
|
+
await handleGetJobs(req, res);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
if (path === "/api/stats" && req.method === "GET") {
|
|
625
|
+
await handleGetStats(req, res);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
const jobMatch = path.match(/^\/api\/jobs\/([^/]+)$/);
|
|
629
|
+
if (jobMatch && req.method === "GET") {
|
|
630
|
+
await handleGetJob(req, res, jobMatch[1]);
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
const pauseMatch = path.match(/^\/api\/workers\/([^/]+)\/pause$/);
|
|
634
|
+
if (pauseMatch && req.method === "POST") {
|
|
635
|
+
await handlePauseWorker(res, pauseMatch[1]);
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
const resumeMatch = path.match(/^\/api\/workers\/([^/]+)\/resume$/);
|
|
639
|
+
if (resumeMatch && req.method === "POST") {
|
|
640
|
+
await handleResumeWorker(res, resumeMatch[1]);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (path === "/" || path === "/index.html") {
|
|
644
|
+
await serveStatic(res, "index.html");
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (path === "/style.css") {
|
|
648
|
+
await serveStatic(res, "style.css");
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
if (path === "/app.js") {
|
|
652
|
+
await serveStatic(res, "app.js");
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
sendError(res, "Not found", 404);
|
|
656
|
+
return true;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("[workmatic] Dashboard error:", error);
|
|
659
|
+
sendError(res, "Internal server error", 500);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function createDashboard(options) {
|
|
665
|
+
const {
|
|
666
|
+
db,
|
|
667
|
+
port = 3e3,
|
|
668
|
+
workers = []
|
|
669
|
+
} = options;
|
|
670
|
+
if (!db) {
|
|
671
|
+
throw new Error("Database instance is required");
|
|
672
|
+
}
|
|
673
|
+
const workerMap = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const worker of workers) {
|
|
675
|
+
workerMap.set(worker.queue, worker);
|
|
676
|
+
}
|
|
677
|
+
const handleRequest = createRequestHandler(db, workerMap);
|
|
678
|
+
const server = (0, import_http.createServer)((req, res) => {
|
|
679
|
+
handleRequest(req, res).catch((error) => {
|
|
680
|
+
console.error("[workmatic] Unhandled error:", error);
|
|
681
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
682
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
server.listen(port);
|
|
686
|
+
return {
|
|
687
|
+
async close() {
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
server.close((err) => {
|
|
690
|
+
if (err) reject(err);
|
|
691
|
+
else resolve();
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
get port() {
|
|
696
|
+
return port;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function createDashboardMiddleware(options) {
|
|
701
|
+
const {
|
|
702
|
+
db,
|
|
703
|
+
workers = [],
|
|
704
|
+
basePath = ""
|
|
705
|
+
} = options;
|
|
706
|
+
if (!db) {
|
|
707
|
+
throw new Error("Database instance is required");
|
|
708
|
+
}
|
|
709
|
+
const workerMap = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const worker of workers) {
|
|
711
|
+
workerMap.set(worker.queue, worker);
|
|
712
|
+
}
|
|
713
|
+
const handleRequest = createRequestHandler(db, workerMap, basePath);
|
|
714
|
+
return (req, res, next) => {
|
|
715
|
+
handleRequest(req, res, next).catch((error) => {
|
|
716
|
+
console.error("[workmatic] Unhandled error:", error);
|
|
717
|
+
if (next) {
|
|
718
|
+
next();
|
|
719
|
+
} else {
|
|
720
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
721
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
727
|
+
0 && (module.exports = {
|
|
728
|
+
createClient,
|
|
729
|
+
createDashboard,
|
|
730
|
+
createDashboardMiddleware,
|
|
731
|
+
createDatabase,
|
|
732
|
+
createWorker,
|
|
733
|
+
defaultBackoff,
|
|
734
|
+
validatePayload
|
|
735
|
+
});
|
|
736
|
+
//# sourceMappingURL=index.cjs.map
|