@nicnocquee/dataqueue 1.22.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
  */
@@ -242,6 +304,42 @@ interface JobQueue<PayloadMap> {
242
304
  * - This will set the job status to 'cancelled' and clear the locked_at and locked_by.
243
305
  */
244
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>;
245
343
  /**
246
344
  * Reclaim stuck jobs.
247
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'.
@@ -288,9 +386,60 @@ interface JobQueue<PayloadMap> {
288
386
  getPool: () => Pool;
289
387
  }
290
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
+
291
440
  /**
292
441
  * Initialize the job queue system
293
442
  */
294
443
  declare const initJobQueue: <PayloadMap = any>(config: JobQueueConfig) => JobQueue<PayloadMap>;
295
444
 
296
- export { type DatabaseSSLConfig, 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
  */
@@ -242,6 +304,42 @@ interface JobQueue<PayloadMap> {
242
304
  * - This will set the job status to 'cancelled' and clear the locked_at and locked_by.
243
305
  */
244
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>;
245
343
  /**
246
344
  * Reclaim stuck jobs.
247
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'.
@@ -288,9 +386,60 @@ interface JobQueue<PayloadMap> {
288
386
  getPool: () => Pool;
289
387
  }
290
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
+
291
440
  /**
292
441
  * Initialize the job queue system
293
442
  */
294
443
  declare const initJobQueue: <PayloadMap = any>(config: JobQueueConfig) => JobQueue<PayloadMap>;
295
444
 
296
- export { type DatabaseSSLConfig, 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 };