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