workmatic 1.0.5 → 1.0.7

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.d.cts CHANGED
@@ -1,11 +1,12 @@
1
- import * as http from 'http';
2
1
  import * as better_sqlite3 from 'better-sqlite3';
2
+ import better_sqlite3__default from 'better-sqlite3';
3
+ import * as http from 'http';
3
4
  import { Kysely, Generated } from 'kysely';
4
5
 
5
6
  /**
6
7
  * Job status types
7
8
  */
8
- type JobStatus = 'ready' | 'running' | 'done' | 'failed' | 'dead';
9
+ type JobStatus = 'ready' | 'running' | 'done' | 'dead';
9
10
  /**
10
11
  * Database table schema for workmatic_jobs
11
12
  */
@@ -63,7 +64,7 @@ interface Job<TPayload = unknown> {
63
64
  maxAttempts: number;
64
65
  /** When the job was created (unix ms) */
65
66
  createdAt: number;
66
- /** Last error message if failed */
67
+ /** Last error from a previous attempt (e.g. before retry) */
67
68
  lastError: string | null;
68
69
  }
69
70
  /**
@@ -84,6 +85,13 @@ interface AddJobResult {
84
85
  ok: true;
85
86
  id: string;
86
87
  }
88
+ /**
89
+ * Result of adding multiple jobs in one transaction
90
+ */
91
+ interface AddManyResult {
92
+ ok: true;
93
+ ids: string[];
94
+ }
87
95
  /**
88
96
  * Options for creating a database
89
97
  */
@@ -124,7 +132,7 @@ interface WorkerOptions {
124
132
  leaseMs?: number;
125
133
  /** Poll interval in ms when no jobs available. Default: 1000 */
126
134
  pollMs?: number;
127
- /** Job execution timeout in ms. Default: undefined (no timeout) */
135
+ /** Job execution timeout in ms. Default: 60000 (1 min). Set to 0 to disable. */
128
136
  timeoutMs?: number;
129
137
  /** Backoff function for retries. Default: exponential */
130
138
  backoff?: BackoffFunction;
@@ -132,6 +140,18 @@ interface WorkerOptions {
132
140
  persistState?: boolean;
133
141
  /** Auto-restore worker state on creation. Default: true (only applies if persistState is true) */
134
142
  autoRestore?: boolean;
143
+ /**
144
+ * Minimum interval between database pause checks (CLI `pause`). Reduces round-trips while pumping.
145
+ * Default: 300 ms
146
+ */
147
+ pauseCheckIntervalMs?: number;
148
+ /**
149
+ * Minimum interval between lease requeue scans. Default: every pump (0).
150
+ * Set to e.g. 1000 to run expired-lease recovery at most once per second.
151
+ */
152
+ requeueExpiredIntervalMs?: number;
153
+ /** Called when the pump loop catches an error (after optional default logging) */
154
+ onPumpError?: (error: unknown) => void;
135
155
  }
136
156
  /**
137
157
  * Job processor function type
@@ -144,7 +164,6 @@ interface JobStats {
144
164
  ready: number;
145
165
  running: number;
146
166
  done: number;
147
- failed: number;
148
167
  dead: number;
149
168
  total: number;
150
169
  }
@@ -154,6 +173,11 @@ interface JobStats {
154
173
  interface WorkmaticClient {
155
174
  /** Add a job to the queue */
156
175
  add<TPayload = unknown>(payload: TPayload, options?: AddJobOptions): Promise<AddJobResult>;
176
+ /**
177
+ * Add many jobs in a single transaction (shared priority, delay, maxAttempts).
178
+ * Faster than repeated `add()` when inserting large batches.
179
+ */
180
+ addMany<TPayload = unknown>(payloads: TPayload[], options?: AddJobOptions): Promise<AddManyResult>;
157
181
  /** Get job statistics */
158
182
  stats(): Promise<JobStats>;
159
183
  /** Clear all jobs from the queue */
@@ -235,6 +259,9 @@ interface ClaimedJob {
235
259
  payload: string;
236
260
  attempts: number;
237
261
  max_attempts: number;
262
+ priority: number;
263
+ created_at: number;
264
+ last_error: string | null;
238
265
  }
239
266
 
240
267
  /**
@@ -258,6 +285,12 @@ interface ClaimedJob {
258
285
  * ```
259
286
  */
260
287
  declare function createDatabase(options?: DatabaseOptions): WorkmaticDb;
288
+ /**
289
+ * Get the underlying better-sqlite3 database instance from a Kysely instance
290
+ * created with {@link createDatabase}. For manually constructed `Kysely` instances,
291
+ * falls back to reading the dialect adapter (may break across Kysely versions).
292
+ */
293
+ declare function getUnderlyingDb(db: WorkmaticDb): better_sqlite3__default.Database;
261
294
 
262
295
  /**
263
296
  * Create a job queue client for adding jobs
@@ -286,6 +319,8 @@ declare function createDatabase(options?: DatabaseOptions): WorkmaticDb;
286
319
  */
287
320
  declare function createClient(options: ClientOptions): WorkmaticClient;
288
321
 
322
+ /** Default job execution timeout when `timeoutMs` is omitted (1 minute). Use `timeoutMs: 0` for no limit. */
323
+ declare const DEFAULT_WORKER_TIMEOUT_MS = 60000;
289
324
  /**
290
325
  * Create a job queue worker for processing jobs
291
326
  *
@@ -383,4 +418,4 @@ declare const defaultBackoff: BackoffFunction;
383
418
  */
384
419
  declare function validatePayload(payload: unknown): string;
385
420
 
386
- export { type AddJobOptions, type AddJobResult, type BackoffFunction, type ClaimedJob, type ClientOptions, type DashboardMiddleware, type DashboardMiddlewareOptions, type DashboardOptions, type DatabaseOptions, type Job, type JobProcessor, type JobStats, type JobStatus, type WorkerOptions, type WorkerState, type WorkmaticClient, type WorkmaticDashboard, type WorkmaticDatabase, type WorkmaticDb, type WorkmaticJobsTable, type WorkmaticWorker, createClient, createDashboard, createDashboardMiddleware, createDatabase, createWorker, defaultBackoff, validatePayload };
421
+ export { type AddJobOptions, type AddJobResult, type AddManyResult, type BackoffFunction, type ClaimedJob, type ClientOptions, DEFAULT_WORKER_TIMEOUT_MS, type DashboardMiddleware, type DashboardMiddlewareOptions, type DashboardOptions, type DatabaseOptions, type Job, type JobProcessor, type JobStats, type JobStatus, type WorkerOptions, type WorkerState, type WorkmaticClient, type WorkmaticDashboard, type WorkmaticDatabase, type WorkmaticDb, type WorkmaticJobsTable, type WorkmaticWorker, createClient, createDashboard, createDashboardMiddleware, createDatabase, createWorker, defaultBackoff, getUnderlyingDb, validatePayload };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- import * as http from 'http';
2
1
  import * as better_sqlite3 from 'better-sqlite3';
2
+ import better_sqlite3__default from 'better-sqlite3';
3
+ import * as http from 'http';
3
4
  import { Kysely, Generated } from 'kysely';
4
5
 
5
6
  /**
6
7
  * Job status types
7
8
  */
8
- type JobStatus = 'ready' | 'running' | 'done' | 'failed' | 'dead';
9
+ type JobStatus = 'ready' | 'running' | 'done' | 'dead';
9
10
  /**
10
11
  * Database table schema for workmatic_jobs
11
12
  */
@@ -63,7 +64,7 @@ interface Job<TPayload = unknown> {
63
64
  maxAttempts: number;
64
65
  /** When the job was created (unix ms) */
65
66
  createdAt: number;
66
- /** Last error message if failed */
67
+ /** Last error from a previous attempt (e.g. before retry) */
67
68
  lastError: string | null;
68
69
  }
69
70
  /**
@@ -84,6 +85,13 @@ interface AddJobResult {
84
85
  ok: true;
85
86
  id: string;
86
87
  }
88
+ /**
89
+ * Result of adding multiple jobs in one transaction
90
+ */
91
+ interface AddManyResult {
92
+ ok: true;
93
+ ids: string[];
94
+ }
87
95
  /**
88
96
  * Options for creating a database
89
97
  */
@@ -124,7 +132,7 @@ interface WorkerOptions {
124
132
  leaseMs?: number;
125
133
  /** Poll interval in ms when no jobs available. Default: 1000 */
126
134
  pollMs?: number;
127
- /** Job execution timeout in ms. Default: undefined (no timeout) */
135
+ /** Job execution timeout in ms. Default: 60000 (1 min). Set to 0 to disable. */
128
136
  timeoutMs?: number;
129
137
  /** Backoff function for retries. Default: exponential */
130
138
  backoff?: BackoffFunction;
@@ -132,6 +140,18 @@ interface WorkerOptions {
132
140
  persistState?: boolean;
133
141
  /** Auto-restore worker state on creation. Default: true (only applies if persistState is true) */
134
142
  autoRestore?: boolean;
143
+ /**
144
+ * Minimum interval between database pause checks (CLI `pause`). Reduces round-trips while pumping.
145
+ * Default: 300 ms
146
+ */
147
+ pauseCheckIntervalMs?: number;
148
+ /**
149
+ * Minimum interval between lease requeue scans. Default: every pump (0).
150
+ * Set to e.g. 1000 to run expired-lease recovery at most once per second.
151
+ */
152
+ requeueExpiredIntervalMs?: number;
153
+ /** Called when the pump loop catches an error (after optional default logging) */
154
+ onPumpError?: (error: unknown) => void;
135
155
  }
136
156
  /**
137
157
  * Job processor function type
@@ -144,7 +164,6 @@ interface JobStats {
144
164
  ready: number;
145
165
  running: number;
146
166
  done: number;
147
- failed: number;
148
167
  dead: number;
149
168
  total: number;
150
169
  }
@@ -154,6 +173,11 @@ interface JobStats {
154
173
  interface WorkmaticClient {
155
174
  /** Add a job to the queue */
156
175
  add<TPayload = unknown>(payload: TPayload, options?: AddJobOptions): Promise<AddJobResult>;
176
+ /**
177
+ * Add many jobs in a single transaction (shared priority, delay, maxAttempts).
178
+ * Faster than repeated `add()` when inserting large batches.
179
+ */
180
+ addMany<TPayload = unknown>(payloads: TPayload[], options?: AddJobOptions): Promise<AddManyResult>;
157
181
  /** Get job statistics */
158
182
  stats(): Promise<JobStats>;
159
183
  /** Clear all jobs from the queue */
@@ -235,6 +259,9 @@ interface ClaimedJob {
235
259
  payload: string;
236
260
  attempts: number;
237
261
  max_attempts: number;
262
+ priority: number;
263
+ created_at: number;
264
+ last_error: string | null;
238
265
  }
239
266
 
240
267
  /**
@@ -258,6 +285,12 @@ interface ClaimedJob {
258
285
  * ```
259
286
  */
260
287
  declare function createDatabase(options?: DatabaseOptions): WorkmaticDb;
288
+ /**
289
+ * Get the underlying better-sqlite3 database instance from a Kysely instance
290
+ * created with {@link createDatabase}. For manually constructed `Kysely` instances,
291
+ * falls back to reading the dialect adapter (may break across Kysely versions).
292
+ */
293
+ declare function getUnderlyingDb(db: WorkmaticDb): better_sqlite3__default.Database;
261
294
 
262
295
  /**
263
296
  * Create a job queue client for adding jobs
@@ -286,6 +319,8 @@ declare function createDatabase(options?: DatabaseOptions): WorkmaticDb;
286
319
  */
287
320
  declare function createClient(options: ClientOptions): WorkmaticClient;
288
321
 
322
+ /** Default job execution timeout when `timeoutMs` is omitted (1 minute). Use `timeoutMs: 0` for no limit. */
323
+ declare const DEFAULT_WORKER_TIMEOUT_MS = 60000;
289
324
  /**
290
325
  * Create a job queue worker for processing jobs
291
326
  *
@@ -383,4 +418,4 @@ declare const defaultBackoff: BackoffFunction;
383
418
  */
384
419
  declare function validatePayload(payload: unknown): string;
385
420
 
386
- export { type AddJobOptions, type AddJobResult, type BackoffFunction, type ClaimedJob, type ClientOptions, type DashboardMiddleware, type DashboardMiddlewareOptions, type DashboardOptions, type DatabaseOptions, type Job, type JobProcessor, type JobStats, type JobStatus, type WorkerOptions, type WorkerState, type WorkmaticClient, type WorkmaticDashboard, type WorkmaticDatabase, type WorkmaticDb, type WorkmaticJobsTable, type WorkmaticWorker, createClient, createDashboard, createDashboardMiddleware, createDatabase, createWorker, defaultBackoff, validatePayload };
421
+ export { type AddJobOptions, type AddJobResult, type AddManyResult, type BackoffFunction, type ClaimedJob, type ClientOptions, DEFAULT_WORKER_TIMEOUT_MS, type DashboardMiddleware, type DashboardMiddlewareOptions, type DashboardOptions, type DatabaseOptions, type Job, type JobProcessor, type JobStats, type JobStatus, type WorkerOptions, type WorkerState, type WorkmaticClient, type WorkmaticDashboard, type WorkmaticDatabase, type WorkmaticDb, type WorkmaticJobsTable, type WorkmaticWorker, createClient, createDashboard, createDashboardMiddleware, createDatabase, createWorker, defaultBackoff, getUnderlyingDb, validatePayload };
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,7 @@ 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;
167
225
  function createWorker(options) {
168
226
  const {
169
227
  db,
@@ -171,10 +229,13 @@ function createWorker(options) {
171
229
  concurrency = 1,
172
230
  leaseMs = 3e4,
173
231
  pollMs = 1e3,
174
- timeoutMs,
232
+ timeoutMs = DEFAULT_WORKER_TIMEOUT_MS,
175
233
  backoff = defaultBackoff,
176
234
  persistState = false,
177
- autoRestore = true
235
+ autoRestore = true,
236
+ pauseCheckIntervalMs = 300,
237
+ requeueExpiredIntervalMs = 0,
238
+ onPumpError
178
239
  } = options;
179
240
  if (!db) {
180
241
  throw new Error("Database instance is required");
@@ -184,6 +245,13 @@ function createWorker(options) {
184
245
  let processor = null;
185
246
  let pumpTimeout = null;
186
247
  let fastqQueue = null;
248
+ let lastPauseCheckAt = 0;
249
+ let cachedDbPaused = false;
250
+ let lastRequeueAt = 0;
251
+ function notifyPumpError(error) {
252
+ console.error("[workmatic] Pump error:", error);
253
+ onPumpError?.(error);
254
+ }
187
255
  function getStateKey() {
188
256
  return `worker_state_${queue}`;
189
257
  }
@@ -191,8 +259,6 @@ function createWorker(options) {
191
259
  if (!persistState) return;
192
260
  const timestamp = now();
193
261
  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
262
  await sql2`
197
263
  INSERT INTO workmatic_settings (queue, paused, updated_at)
198
264
  VALUES (${key}, ${state === "paused" ? 1 : state === "running" ? 2 : 0}, ${timestamp})
@@ -227,17 +293,34 @@ function createWorker(options) {
227
293
  const timestamp = now();
228
294
  const leaseUntil = timestamp + leaseMs;
229
295
  return await db.transaction().execute(async (trx) => {
230
- 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();
231
- if (jobs.length === 0) {
296
+ const result = await sql2`
297
+ UPDATE workmatic_jobs
298
+ SET status = 'running', lease_until = ${leaseUntil}, updated_at = ${timestamp}
299
+ WHERE rowid IN (
300
+ SELECT rowid FROM workmatic_jobs
301
+ WHERE queue = ${queue}
302
+ AND status = 'ready'
303
+ AND run_at <= ${timestamp}
304
+ ORDER BY priority ASC, id ASC
305
+ LIMIT ${limit}
306
+ )
307
+ RETURNING id, public_id, queue, payload, attempts, max_attempts, priority, created_at, last_error
308
+ `.execute(trx);
309
+ const rows = result.rows;
310
+ if (!rows || !Array.isArray(rows)) {
232
311
  return [];
233
312
  }
234
- const jobIds = jobs.map((j) => j.id);
235
- await trx.updateTable("workmatic_jobs").set({
236
- status: "running",
237
- lease_until: leaseUntil,
238
- updated_at: timestamp
239
- }).where("id", "in", jobIds).execute();
240
- return jobs;
313
+ return rows.map((row) => ({
314
+ id: row.id,
315
+ public_id: row.public_id,
316
+ queue: row.queue,
317
+ payload: row.payload,
318
+ attempts: row.attempts,
319
+ max_attempts: row.max_attempts,
320
+ priority: row.priority,
321
+ created_at: row.created_at,
322
+ last_error: row.last_error
323
+ }));
241
324
  });
242
325
  }
243
326
  async function markDone(jobId) {
@@ -294,13 +377,11 @@ function createWorker(options) {
294
377
  queue: claimedJob.queue,
295
378
  payload,
296
379
  status: "running",
297
- priority: 0,
298
- // Not needed for processing
380
+ priority: claimedJob.priority,
299
381
  attempts: claimedJob.attempts,
300
382
  maxAttempts: claimedJob.max_attempts,
301
- createdAt: 0,
302
- // Not needed for processing
303
- lastError: null
383
+ createdAt: claimedJob.created_at,
384
+ lastError: claimedJob.last_error
304
385
  };
305
386
  try {
306
387
  if (timeoutMs) {
@@ -331,12 +412,21 @@ function createWorker(options) {
331
412
  return;
332
413
  }
333
414
  try {
334
- const dbPaused = await isQueuePausedInDb();
335
- if (dbPaused) {
415
+ const t = now();
416
+ if (t - lastPauseCheckAt >= pauseCheckIntervalMs) {
417
+ lastPauseCheckAt = t;
418
+ cachedDbPaused = await isQueuePausedInDb();
419
+ }
420
+ if (cachedDbPaused) {
336
421
  pumpTimeout = setTimeout(pump, pollMs);
337
422
  return;
338
423
  }
339
- await requeueExpiredLeases();
424
+ if (requeueExpiredIntervalMs <= 0 || t - lastRequeueAt >= requeueExpiredIntervalMs) {
425
+ if (requeueExpiredIntervalMs > 0) {
426
+ lastRequeueAt = t;
427
+ }
428
+ await requeueExpiredLeases();
429
+ }
340
430
  const batchSize = concurrency * 2;
341
431
  const jobs = await claimBatch(batchSize);
342
432
  if (jobs.length > 0) {
@@ -348,7 +438,7 @@ function createWorker(options) {
348
438
  pumpTimeout = setTimeout(pump, pollMs);
349
439
  }
350
440
  } catch (error) {
351
- console.error("[workmatic] Pump error:", error);
441
+ notifyPumpError(error);
352
442
  pumpTimeout = setTimeout(pump, pollMs);
353
443
  }
354
444
  }
@@ -404,7 +494,6 @@ function createWorker(options) {
404
494
  ready: 0,
405
495
  running: 0,
406
496
  done: 0,
407
- failed: 0,
408
497
  dead: 0,
409
498
  total: 0
410
499
  };
@@ -547,7 +636,6 @@ function createRequestHandler(db, workerMap, basePath = "") {
547
636
  ready: 0,
548
637
  running: 0,
549
638
  done: 0,
550
- failed: 0,
551
639
  dead: 0,
552
640
  total: 0
553
641
  };
@@ -753,12 +841,14 @@ function createDashboardMiddleware(options) {
753
841
  };
754
842
  }
755
843
  export {
844
+ DEFAULT_WORKER_TIMEOUT_MS,
756
845
  createClient,
757
846
  createDashboard,
758
847
  createDashboardMiddleware,
759
848
  createDatabase,
760
849
  createWorker,
761
850
  defaultBackoff,
851
+ getUnderlyingDb,
762
852
  validatePayload
763
853
  };
764
854
  //# sourceMappingURL=index.js.map