@nicnocquee/dataqueue 1.21.0 → 1.24.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/dist/index.d.cts CHANGED
@@ -11,18 +11,75 @@ interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
11
11
  * Timeout for this job in milliseconds. If not set, uses the processor default or unlimited.
12
12
  */
13
13
  timeoutMs?: number;
14
+ /**
15
+ * If true, the job will be forcefully terminated (using Worker Threads) when timeout is reached.
16
+ * If false (default), the job will only receive an AbortSignal and must handle the abort gracefully.
17
+ *
18
+ * **⚠️ RUNTIME REQUIREMENTS**: This option requires **Node.js** and uses the `worker_threads` module.
19
+ * It will **not work** in Bun or other runtimes that don't support Node.js worker threads.
20
+ *
21
+ * **IMPORTANT**: When `forceKillOnTimeout` is true, the handler must be serializable. This means:
22
+ * - The handler should be a standalone function (not a closure over external variables)
23
+ * - It should not capture variables from outer scopes that reference external dependencies
24
+ * - It should not use 'this' context unless it's a bound method
25
+ * - All dependencies must be importable in the worker thread context
26
+ *
27
+ * **Examples of serializable handlers:**
28
+ * ```ts
29
+ * // ✅ Good - standalone function
30
+ * const handler = async (payload, signal) => {
31
+ * await doSomething(payload);
32
+ * };
33
+ *
34
+ * // ✅ Good - function that imports dependencies
35
+ * const handler = async (payload, signal) => {
36
+ * const { api } = await import('./api');
37
+ * await api.call(payload);
38
+ * };
39
+ *
40
+ * // ❌ Bad - closure over external variable
41
+ * const db = getDatabase();
42
+ * const handler = async (payload, signal) => {
43
+ * await db.query(payload); // 'db' is captured from closure
44
+ * };
45
+ *
46
+ * // ❌ Bad - uses 'this' context
47
+ * class MyHandler {
48
+ * async handle(payload, signal) {
49
+ * await this.doSomething(payload); // 'this' won't work
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * If your handler doesn't meet these requirements, use `forceKillOnTimeout: false` (default)
55
+ * and ensure your handler checks `signal.aborted` to exit gracefully.
56
+ *
57
+ * Note: forceKillOnTimeout requires timeoutMs to be set.
58
+ */
59
+ forceKillOnTimeout?: boolean;
14
60
  /**
15
61
  * Tags for this job. Used for grouping, searching, or batch operations.
16
62
  */
17
63
  tags?: string[];
18
64
  }
65
+ /**
66
+ * Options for editing a pending job.
67
+ * All fields are optional and only provided fields will be updated.
68
+ * Note: jobType cannot be changed.
69
+ * timeoutMs and tags can be set to null to clear them.
70
+ */
71
+ type EditJobOptions<PayloadMap, T extends JobType<PayloadMap>> = Partial<Omit<JobOptions<PayloadMap, T>, 'jobType'>> & {
72
+ timeoutMs?: number | null;
73
+ tags?: string[] | null;
74
+ };
19
75
  declare enum JobEventType {
20
76
  Added = "added",
21
77
  Processing = "processing",
22
78
  Completed = "completed",
23
79
  Failed = "failed",
24
80
  Cancelled = "cancelled",
25
- Retried = "retried"
81
+ Retried = "retried",
82
+ Edited = "edited"
26
83
  }
27
84
  interface JobEvent {
28
85
  id: number;
@@ -60,6 +117,11 @@ interface JobRecord<PayloadMap, T extends JobType<PayloadMap>> {
60
117
  * Timeout for this job in milliseconds (null means no timeout).
61
118
  */
62
119
  timeoutMs?: number | null;
120
+ /**
121
+ * If true, the job will be forcefully terminated (using Worker Threads) when timeout is reached.
122
+ * If false (default), the job will only receive an AbortSignal and must handle the abort gracefully.
123
+ */
124
+ forceKillOnTimeout?: boolean | null;
63
125
  /**
64
126
  * The reason for the last failure, if any.
65
127
  */
@@ -147,6 +209,24 @@ interface Processor {
147
209
  */
148
210
  start: () => Promise<number>;
149
211
  }
212
+ interface DatabaseSSLConfig {
213
+ /**
214
+ * CA certificate as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
215
+ */
216
+ ca?: string;
217
+ /**
218
+ * Client certificate as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
219
+ */
220
+ cert?: string;
221
+ /**
222
+ * Client private key as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
223
+ */
224
+ key?: string;
225
+ /**
226
+ * Whether to reject unauthorized certificates (default: true)
227
+ */
228
+ rejectUnauthorized?: boolean;
229
+ }
150
230
  interface JobQueueConfig {
151
231
  databaseConfig: {
152
232
  connectionString?: string;
@@ -155,7 +235,7 @@ interface JobQueueConfig {
155
235
  database?: string;
156
236
  user?: string;
157
237
  password?: string;
158
- ssl?: any;
238
+ ssl?: DatabaseSSLConfig;
159
239
  };
160
240
  verbose?: boolean;
161
241
  }
@@ -224,6 +304,42 @@ interface JobQueue<PayloadMap> {
224
304
  * - This will set the job status to 'cancelled' and clear the locked_at and locked_by.
225
305
  */
226
306
  cancelJob: (jobId: number) => Promise<void>;
307
+ /**
308
+ * Edit a pending job given its ID.
309
+ * - Only works for jobs with status 'pending'. Silently fails for other statuses.
310
+ * - All fields in EditJobOptions are optional - only provided fields will be updated.
311
+ * - jobType cannot be changed.
312
+ * - Records an 'edited' event with the updated fields in metadata.
313
+ */
314
+ editJob: <T extends JobType<PayloadMap>>(jobId: number, updates: EditJobOptions<PayloadMap, T>) => Promise<void>;
315
+ /**
316
+ * Edit all pending jobs that match the filters.
317
+ * - Only works for jobs with status 'pending'. Non-pending jobs are not affected.
318
+ * - All fields in EditJobOptions are optional - only provided fields will be updated.
319
+ * - jobType cannot be changed.
320
+ * - Records an 'edited' event with the updated fields in metadata for each affected job.
321
+ * - Returns the number of jobs that were edited.
322
+ * - The filters are:
323
+ * - jobType: The job type to edit.
324
+ * - priority: The priority of the job to edit.
325
+ * - runAt: The time the job is scheduled to run at (now supports gt/gte/lt/lte/eq).
326
+ * - tags: An object with 'values' (string[]) and 'mode' (TagQueryMode) for tag-based editing.
327
+ */
328
+ editAllPendingJobs: <T extends JobType<PayloadMap>>(filters: {
329
+ jobType?: string;
330
+ priority?: number;
331
+ runAt?: Date | {
332
+ gt?: Date;
333
+ gte?: Date;
334
+ lt?: Date;
335
+ lte?: Date;
336
+ eq?: Date;
337
+ };
338
+ tags?: {
339
+ values: string[];
340
+ mode?: TagQueryMode;
341
+ };
342
+ } | undefined, updates: EditJobOptions<PayloadMap, T>) => Promise<number>;
227
343
  /**
228
344
  * Reclaim stuck jobs.
229
345
  * - If a process (e.g., API route or worker) crashes after marking a job as 'processing' but before completing it, the job can remain stuck in the 'processing' state indefinitely. This can happen if the process is killed or encounters an unhandled error after updating the job status but before marking it as 'completed' or 'failed'.
@@ -270,9 +386,60 @@ interface JobQueue<PayloadMap> {
270
386
  getPool: () => Pool;
271
387
  }
272
388
 
389
+ /**
390
+ * Validates that a job handler can be serialized for use with forceKillOnTimeout.
391
+ *
392
+ * This function checks if a handler can be safely serialized and executed in a worker thread.
393
+ * Use this function during development to catch serialization issues early.
394
+ *
395
+ * @param handler - The job handler function to validate
396
+ * @param jobType - Optional job type name for better error messages
397
+ * @returns An object with `isSerializable` boolean and optional `error` message
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * const handler = async (payload, signal) => {
402
+ * await doSomething(payload);
403
+ * };
404
+ *
405
+ * const result = validateHandlerSerializable(handler, 'myJob');
406
+ * if (!result.isSerializable) {
407
+ * console.error('Handler is not serializable:', result.error);
408
+ * }
409
+ * ```
410
+ */
411
+ declare function validateHandlerSerializable<PayloadMap, T extends keyof PayloadMap & string>(handler: JobHandler<PayloadMap, T>, jobType?: string): {
412
+ isSerializable: boolean;
413
+ error?: string;
414
+ };
415
+ /**
416
+ * Test if a handler can be serialized and executed in a worker thread.
417
+ * This is a more thorough check that actually attempts to serialize and deserialize the handler.
418
+ *
419
+ * @param handler - The job handler function to test
420
+ * @param jobType - Optional job type name for better error messages
421
+ * @returns Promise that resolves to validation result
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * const handler = async (payload, signal) => {
426
+ * await doSomething(payload);
427
+ * };
428
+ *
429
+ * const result = await testHandlerSerialization(handler, 'myJob');
430
+ * if (!result.isSerializable) {
431
+ * console.error('Handler failed serialization test:', result.error);
432
+ * }
433
+ * ```
434
+ */
435
+ declare function testHandlerSerialization<PayloadMap, T extends keyof PayloadMap & string>(handler: JobHandler<PayloadMap, T>, jobType?: string): Promise<{
436
+ isSerializable: boolean;
437
+ error?: string;
438
+ }>;
439
+
273
440
  /**
274
441
  * Initialize the job queue system
275
442
  */
276
443
  declare const initJobQueue: <PayloadMap = any>(config: JobQueueConfig) => JobQueue<PayloadMap>;
277
444
 
278
- export { FailureReason, type JobEvent, JobEventType, type JobHandler, type JobHandlers, type JobOptions, type JobQueue, type JobQueueConfig, type JobRecord, type JobStatus, type JobType, type Processor, type ProcessorOptions, type TagQueryMode, initJobQueue };
445
+ export { type DatabaseSSLConfig, type EditJobOptions, FailureReason, type JobEvent, JobEventType, type JobHandler, type JobHandlers, type JobOptions, type JobQueue, type JobQueueConfig, type JobRecord, type JobStatus, type JobType, type Processor, type ProcessorOptions, type TagQueryMode, initJobQueue, testHandlerSerialization, validateHandlerSerializable };
package/dist/index.d.ts CHANGED
@@ -11,18 +11,75 @@ interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
11
11
  * Timeout for this job in milliseconds. If not set, uses the processor default or unlimited.
12
12
  */
13
13
  timeoutMs?: number;
14
+ /**
15
+ * If true, the job will be forcefully terminated (using Worker Threads) when timeout is reached.
16
+ * If false (default), the job will only receive an AbortSignal and must handle the abort gracefully.
17
+ *
18
+ * **⚠️ RUNTIME REQUIREMENTS**: This option requires **Node.js** and uses the `worker_threads` module.
19
+ * It will **not work** in Bun or other runtimes that don't support Node.js worker threads.
20
+ *
21
+ * **IMPORTANT**: When `forceKillOnTimeout` is true, the handler must be serializable. This means:
22
+ * - The handler should be a standalone function (not a closure over external variables)
23
+ * - It should not capture variables from outer scopes that reference external dependencies
24
+ * - It should not use 'this' context unless it's a bound method
25
+ * - All dependencies must be importable in the worker thread context
26
+ *
27
+ * **Examples of serializable handlers:**
28
+ * ```ts
29
+ * // ✅ Good - standalone function
30
+ * const handler = async (payload, signal) => {
31
+ * await doSomething(payload);
32
+ * };
33
+ *
34
+ * // ✅ Good - function that imports dependencies
35
+ * const handler = async (payload, signal) => {
36
+ * const { api } = await import('./api');
37
+ * await api.call(payload);
38
+ * };
39
+ *
40
+ * // ❌ Bad - closure over external variable
41
+ * const db = getDatabase();
42
+ * const handler = async (payload, signal) => {
43
+ * await db.query(payload); // 'db' is captured from closure
44
+ * };
45
+ *
46
+ * // ❌ Bad - uses 'this' context
47
+ * class MyHandler {
48
+ * async handle(payload, signal) {
49
+ * await this.doSomething(payload); // 'this' won't work
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * If your handler doesn't meet these requirements, use `forceKillOnTimeout: false` (default)
55
+ * and ensure your handler checks `signal.aborted` to exit gracefully.
56
+ *
57
+ * Note: forceKillOnTimeout requires timeoutMs to be set.
58
+ */
59
+ forceKillOnTimeout?: boolean;
14
60
  /**
15
61
  * Tags for this job. Used for grouping, searching, or batch operations.
16
62
  */
17
63
  tags?: string[];
18
64
  }
65
+ /**
66
+ * Options for editing a pending job.
67
+ * All fields are optional and only provided fields will be updated.
68
+ * Note: jobType cannot be changed.
69
+ * timeoutMs and tags can be set to null to clear them.
70
+ */
71
+ type EditJobOptions<PayloadMap, T extends JobType<PayloadMap>> = Partial<Omit<JobOptions<PayloadMap, T>, 'jobType'>> & {
72
+ timeoutMs?: number | null;
73
+ tags?: string[] | null;
74
+ };
19
75
  declare enum JobEventType {
20
76
  Added = "added",
21
77
  Processing = "processing",
22
78
  Completed = "completed",
23
79
  Failed = "failed",
24
80
  Cancelled = "cancelled",
25
- Retried = "retried"
81
+ Retried = "retried",
82
+ Edited = "edited"
26
83
  }
27
84
  interface JobEvent {
28
85
  id: number;
@@ -60,6 +117,11 @@ interface JobRecord<PayloadMap, T extends JobType<PayloadMap>> {
60
117
  * Timeout for this job in milliseconds (null means no timeout).
61
118
  */
62
119
  timeoutMs?: number | null;
120
+ /**
121
+ * If true, the job will be forcefully terminated (using Worker Threads) when timeout is reached.
122
+ * If false (default), the job will only receive an AbortSignal and must handle the abort gracefully.
123
+ */
124
+ forceKillOnTimeout?: boolean | null;
63
125
  /**
64
126
  * The reason for the last failure, if any.
65
127
  */
@@ -147,6 +209,24 @@ interface Processor {
147
209
  */
148
210
  start: () => Promise<number>;
149
211
  }
212
+ interface DatabaseSSLConfig {
213
+ /**
214
+ * CA certificate as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
215
+ */
216
+ ca?: string;
217
+ /**
218
+ * Client certificate as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
219
+ */
220
+ cert?: string;
221
+ /**
222
+ * Client private key as PEM string or file path. If the value starts with 'file://', it will be loaded from file, otherwise treated as PEM string.
223
+ */
224
+ key?: string;
225
+ /**
226
+ * Whether to reject unauthorized certificates (default: true)
227
+ */
228
+ rejectUnauthorized?: boolean;
229
+ }
150
230
  interface JobQueueConfig {
151
231
  databaseConfig: {
152
232
  connectionString?: string;
@@ -155,7 +235,7 @@ interface JobQueueConfig {
155
235
  database?: string;
156
236
  user?: string;
157
237
  password?: string;
158
- ssl?: any;
238
+ ssl?: DatabaseSSLConfig;
159
239
  };
160
240
  verbose?: boolean;
161
241
  }
@@ -224,6 +304,42 @@ interface JobQueue<PayloadMap> {
224
304
  * - This will set the job status to 'cancelled' and clear the locked_at and locked_by.
225
305
  */
226
306
  cancelJob: (jobId: number) => Promise<void>;
307
+ /**
308
+ * Edit a pending job given its ID.
309
+ * - Only works for jobs with status 'pending'. Silently fails for other statuses.
310
+ * - All fields in EditJobOptions are optional - only provided fields will be updated.
311
+ * - jobType cannot be changed.
312
+ * - Records an 'edited' event with the updated fields in metadata.
313
+ */
314
+ editJob: <T extends JobType<PayloadMap>>(jobId: number, updates: EditJobOptions<PayloadMap, T>) => Promise<void>;
315
+ /**
316
+ * Edit all pending jobs that match the filters.
317
+ * - Only works for jobs with status 'pending'. Non-pending jobs are not affected.
318
+ * - All fields in EditJobOptions are optional - only provided fields will be updated.
319
+ * - jobType cannot be changed.
320
+ * - Records an 'edited' event with the updated fields in metadata for each affected job.
321
+ * - Returns the number of jobs that were edited.
322
+ * - The filters are:
323
+ * - jobType: The job type to edit.
324
+ * - priority: The priority of the job to edit.
325
+ * - runAt: The time the job is scheduled to run at (now supports gt/gte/lt/lte/eq).
326
+ * - tags: An object with 'values' (string[]) and 'mode' (TagQueryMode) for tag-based editing.
327
+ */
328
+ editAllPendingJobs: <T extends JobType<PayloadMap>>(filters: {
329
+ jobType?: string;
330
+ priority?: number;
331
+ runAt?: Date | {
332
+ gt?: Date;
333
+ gte?: Date;
334
+ lt?: Date;
335
+ lte?: Date;
336
+ eq?: Date;
337
+ };
338
+ tags?: {
339
+ values: string[];
340
+ mode?: TagQueryMode;
341
+ };
342
+ } | undefined, updates: EditJobOptions<PayloadMap, T>) => Promise<number>;
227
343
  /**
228
344
  * Reclaim stuck jobs.
229
345
  * - If a process (e.g., API route or worker) crashes after marking a job as 'processing' but before completing it, the job can remain stuck in the 'processing' state indefinitely. This can happen if the process is killed or encounters an unhandled error after updating the job status but before marking it as 'completed' or 'failed'.
@@ -270,9 +386,60 @@ interface JobQueue<PayloadMap> {
270
386
  getPool: () => Pool;
271
387
  }
272
388
 
389
+ /**
390
+ * Validates that a job handler can be serialized for use with forceKillOnTimeout.
391
+ *
392
+ * This function checks if a handler can be safely serialized and executed in a worker thread.
393
+ * Use this function during development to catch serialization issues early.
394
+ *
395
+ * @param handler - The job handler function to validate
396
+ * @param jobType - Optional job type name for better error messages
397
+ * @returns An object with `isSerializable` boolean and optional `error` message
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * const handler = async (payload, signal) => {
402
+ * await doSomething(payload);
403
+ * };
404
+ *
405
+ * const result = validateHandlerSerializable(handler, 'myJob');
406
+ * if (!result.isSerializable) {
407
+ * console.error('Handler is not serializable:', result.error);
408
+ * }
409
+ * ```
410
+ */
411
+ declare function validateHandlerSerializable<PayloadMap, T extends keyof PayloadMap & string>(handler: JobHandler<PayloadMap, T>, jobType?: string): {
412
+ isSerializable: boolean;
413
+ error?: string;
414
+ };
415
+ /**
416
+ * Test if a handler can be serialized and executed in a worker thread.
417
+ * This is a more thorough check that actually attempts to serialize and deserialize the handler.
418
+ *
419
+ * @param handler - The job handler function to test
420
+ * @param jobType - Optional job type name for better error messages
421
+ * @returns Promise that resolves to validation result
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * const handler = async (payload, signal) => {
426
+ * await doSomething(payload);
427
+ * };
428
+ *
429
+ * const result = await testHandlerSerialization(handler, 'myJob');
430
+ * if (!result.isSerializable) {
431
+ * console.error('Handler failed serialization test:', result.error);
432
+ * }
433
+ * ```
434
+ */
435
+ declare function testHandlerSerialization<PayloadMap, T extends keyof PayloadMap & string>(handler: JobHandler<PayloadMap, T>, jobType?: string): Promise<{
436
+ isSerializable: boolean;
437
+ error?: string;
438
+ }>;
439
+
273
440
  /**
274
441
  * Initialize the job queue system
275
442
  */
276
443
  declare const initJobQueue: <PayloadMap = any>(config: JobQueueConfig) => JobQueue<PayloadMap>;
277
444
 
278
- export { FailureReason, type JobEvent, JobEventType, type JobHandler, type JobHandlers, type JobOptions, type JobQueue, type JobQueueConfig, type JobRecord, type JobStatus, type JobType, type Processor, type ProcessorOptions, type TagQueryMode, initJobQueue };
445
+ export { type DatabaseSSLConfig, type EditJobOptions, FailureReason, type JobEvent, JobEventType, type JobHandler, type JobHandlers, type JobOptions, type JobQueue, type JobQueueConfig, type JobRecord, type JobStatus, type JobType, type Processor, type ProcessorOptions, type TagQueryMode, initJobQueue, testHandlerSerialization, validateHandlerSerializable };