@nicnocquee/dataqueue 1.31.0 → 1.32.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.cjs +2418 -1936
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -16
- package/dist/index.d.ts +151 -16
- package/dist/index.js +2418 -1936
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/backend.ts +70 -4
- package/src/backends/postgres.ts +345 -29
- package/src/backends/redis-scripts.ts +197 -22
- package/src/backends/redis.test.ts +621 -0
- package/src/backends/redis.ts +400 -21
- package/src/index.ts +12 -29
- package/src/processor.ts +14 -93
- package/src/queue.test.ts +29 -0
- package/src/queue.ts +19 -251
- package/src/types.ts +28 -10
package/src/backends/redis.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
CronScheduleRecord,
|
|
13
13
|
CronScheduleStatus,
|
|
14
14
|
EditCronScheduleOptions,
|
|
15
|
+
WaitpointRecord,
|
|
16
|
+
CreateTokenOptions,
|
|
15
17
|
} from '../types.js';
|
|
16
18
|
import {
|
|
17
19
|
QueueBackend,
|
|
@@ -20,6 +22,43 @@ import {
|
|
|
20
22
|
CronScheduleInput,
|
|
21
23
|
} from '../backend.js';
|
|
22
24
|
import { log } from '../log-context.js';
|
|
25
|
+
|
|
26
|
+
const MAX_TIMEOUT_MS = 365 * 24 * 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
/** Parse a timeout string like '10m', '1h', '24h', '7d' into milliseconds. */
|
|
29
|
+
function parseTimeoutString(timeout: string): number {
|
|
30
|
+
const match = timeout.match(/^(\d+)(s|m|h|d)$/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid timeout format: "${timeout}". Expected format like "10m", "1h", "24h", "7d".`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const value = parseInt(match[1], 10);
|
|
37
|
+
const unit = match[2];
|
|
38
|
+
let ms: number;
|
|
39
|
+
switch (unit) {
|
|
40
|
+
case 's':
|
|
41
|
+
ms = value * 1000;
|
|
42
|
+
break;
|
|
43
|
+
case 'm':
|
|
44
|
+
ms = value * 60 * 1000;
|
|
45
|
+
break;
|
|
46
|
+
case 'h':
|
|
47
|
+
ms = value * 60 * 60 * 1000;
|
|
48
|
+
break;
|
|
49
|
+
case 'd':
|
|
50
|
+
ms = value * 24 * 60 * 60 * 1000;
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unknown timeout unit: "${unit}"`);
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isFinite(ms) || ms > MAX_TIMEOUT_MS) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Timeout value "${timeout}" is too large. Maximum allowed is 365 days.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return ms;
|
|
61
|
+
}
|
|
23
62
|
import {
|
|
24
63
|
ADD_JOB_SCRIPT,
|
|
25
64
|
GET_NEXT_BATCH_SCRIPT,
|
|
@@ -29,8 +68,12 @@ import {
|
|
|
29
68
|
CANCEL_JOB_SCRIPT,
|
|
30
69
|
PROLONG_JOB_SCRIPT,
|
|
31
70
|
RECLAIM_STUCK_JOBS_SCRIPT,
|
|
32
|
-
|
|
71
|
+
CLEANUP_OLD_JOBS_BATCH_SCRIPT,
|
|
72
|
+
WAIT_JOB_SCRIPT,
|
|
73
|
+
COMPLETE_WAITPOINT_SCRIPT,
|
|
74
|
+
EXPIRE_TIMED_OUT_WAITPOINTS_SCRIPT,
|
|
33
75
|
} from './redis-scripts.js';
|
|
76
|
+
import { randomUUID } from 'crypto';
|
|
34
77
|
|
|
35
78
|
/** Helper: convert a Redis hash flat array [k,v,k,v,...] to a JS object */
|
|
36
79
|
function hashToObject(arr: string[]): Record<string, string> {
|
|
@@ -116,9 +159,24 @@ function deserializeJob<PayloadMap, T extends JobType<PayloadMap>>(
|
|
|
116
159
|
tags,
|
|
117
160
|
idempotencyKey: nullish(h.idempotencyKey) as string | null | undefined,
|
|
118
161
|
progress: numOrNull(h.progress),
|
|
162
|
+
waitUntil: dateOrNull(h.waitUntil),
|
|
163
|
+
waitTokenId: nullish(h.waitTokenId) as string | null | undefined,
|
|
164
|
+
stepData: parseStepData(h.stepData),
|
|
119
165
|
};
|
|
120
166
|
}
|
|
121
167
|
|
|
168
|
+
/** Parse step data from a Redis hash field. */
|
|
169
|
+
function parseStepData(
|
|
170
|
+
raw: string | undefined,
|
|
171
|
+
): Record<string, any> | undefined {
|
|
172
|
+
if (!raw || raw === 'null') return undefined;
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
export class RedisBackend implements QueueBackend {
|
|
123
181
|
private client: RedisType;
|
|
124
182
|
private prefix: string;
|
|
@@ -322,10 +380,19 @@ export class RedisBackend implements QueueBackend {
|
|
|
322
380
|
if (filters.runAt) {
|
|
323
381
|
jobs = this.filterByRunAt(jobs, filters.runAt);
|
|
324
382
|
}
|
|
383
|
+
// Cursor-based (keyset) pagination: only return jobs with id < cursor
|
|
384
|
+
if (filters.cursor !== undefined) {
|
|
385
|
+
jobs = jobs.filter((j) => j.id < filters.cursor!);
|
|
386
|
+
}
|
|
325
387
|
}
|
|
326
388
|
|
|
327
|
-
// Sort by
|
|
328
|
-
jobs.sort((a, b) => b.
|
|
389
|
+
// Sort by id DESC for consistent keyset pagination (matches Postgres ORDER BY id DESC)
|
|
390
|
+
jobs.sort((a, b) => b.id - a.id);
|
|
391
|
+
|
|
392
|
+
// When using cursor, skip offset
|
|
393
|
+
if (filters?.cursor !== undefined) {
|
|
394
|
+
return jobs.slice(0, limit);
|
|
395
|
+
}
|
|
329
396
|
return jobs.slice(offset, offset + limit);
|
|
330
397
|
}
|
|
331
398
|
|
|
@@ -624,27 +691,115 @@ export class RedisBackend implements QueueBackend {
|
|
|
624
691
|
return count;
|
|
625
692
|
}
|
|
626
693
|
|
|
627
|
-
|
|
694
|
+
/**
|
|
695
|
+
* Delete completed jobs older than the given number of days.
|
|
696
|
+
* Uses SSCAN to iterate the completed set in batches, avoiding
|
|
697
|
+
* loading all IDs into memory and preventing long Redis blocks.
|
|
698
|
+
*
|
|
699
|
+
* @param daysToKeep - Number of days to retain completed jobs (default 30).
|
|
700
|
+
* @param batchSize - Number of IDs to scan per SSCAN iteration (default 200).
|
|
701
|
+
* @returns Total number of deleted jobs.
|
|
702
|
+
*/
|
|
703
|
+
async cleanupOldJobs(daysToKeep = 30, batchSize = 200): Promise<number> {
|
|
628
704
|
const cutoffMs = this.nowMs() - daysToKeep * 24 * 60 * 60 * 1000;
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
705
|
+
const setKey = `${this.prefix}status:completed`;
|
|
706
|
+
let totalDeleted = 0;
|
|
707
|
+
let cursor = '0';
|
|
708
|
+
|
|
709
|
+
do {
|
|
710
|
+
const [nextCursor, ids] = await this.client.sscan(
|
|
711
|
+
setKey,
|
|
712
|
+
cursor,
|
|
713
|
+
'COUNT',
|
|
714
|
+
batchSize,
|
|
715
|
+
);
|
|
716
|
+
cursor = nextCursor;
|
|
717
|
+
|
|
718
|
+
if (ids.length > 0) {
|
|
719
|
+
const result = (await this.client.eval(
|
|
720
|
+
CLEANUP_OLD_JOBS_BATCH_SCRIPT,
|
|
721
|
+
1,
|
|
722
|
+
this.prefix,
|
|
723
|
+
cutoffMs,
|
|
724
|
+
...ids,
|
|
725
|
+
)) as number;
|
|
726
|
+
totalDeleted += Number(result);
|
|
727
|
+
}
|
|
728
|
+
} while (cursor !== '0');
|
|
729
|
+
|
|
730
|
+
log(`Deleted ${totalDeleted} old jobs`);
|
|
731
|
+
return totalDeleted;
|
|
637
732
|
}
|
|
638
733
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
734
|
+
/**
|
|
735
|
+
* Delete job events older than the given number of days.
|
|
736
|
+
* Iterates all event lists and removes events whose createdAt is before the cutoff.
|
|
737
|
+
* Also removes orphaned event lists (where the job no longer exists).
|
|
738
|
+
*
|
|
739
|
+
* @param daysToKeep - Number of days to retain events (default 30).
|
|
740
|
+
* @param batchSize - Number of event keys to scan per SCAN iteration (default 200).
|
|
741
|
+
* @returns Total number of deleted events.
|
|
742
|
+
*/
|
|
743
|
+
async cleanupOldJobEvents(daysToKeep = 30, batchSize = 200): Promise<number> {
|
|
744
|
+
const cutoffMs = this.nowMs() - daysToKeep * 24 * 60 * 60 * 1000;
|
|
745
|
+
const pattern = `${this.prefix}events:*`;
|
|
746
|
+
let totalDeleted = 0;
|
|
747
|
+
let cursor = '0';
|
|
748
|
+
|
|
749
|
+
do {
|
|
750
|
+
const [nextCursor, keys] = await this.client.scan(
|
|
751
|
+
cursor,
|
|
752
|
+
'MATCH',
|
|
753
|
+
pattern,
|
|
754
|
+
'COUNT',
|
|
755
|
+
batchSize,
|
|
756
|
+
);
|
|
757
|
+
cursor = nextCursor;
|
|
758
|
+
|
|
759
|
+
for (const key of keys) {
|
|
760
|
+
// Check if the job still exists; if not, delete the entire event list
|
|
761
|
+
const jobIdStr = key.slice(`${this.prefix}events:`.length);
|
|
762
|
+
const jobExists = await this.client.exists(
|
|
763
|
+
`${this.prefix}job:${jobIdStr}`,
|
|
764
|
+
);
|
|
765
|
+
if (!jobExists) {
|
|
766
|
+
const len = await this.client.llen(key);
|
|
767
|
+
await this.client.del(key);
|
|
768
|
+
totalDeleted += len;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Filter events by date: read all, keep recent, rewrite
|
|
773
|
+
const events = await this.client.lrange(key, 0, -1);
|
|
774
|
+
const kept: string[] = [];
|
|
775
|
+
for (const raw of events) {
|
|
776
|
+
try {
|
|
777
|
+
const e = JSON.parse(raw);
|
|
778
|
+
if (e.createdAt >= cutoffMs) {
|
|
779
|
+
kept.push(raw);
|
|
780
|
+
} else {
|
|
781
|
+
totalDeleted++;
|
|
782
|
+
}
|
|
783
|
+
} catch {
|
|
784
|
+
totalDeleted++;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (kept.length === 0) {
|
|
789
|
+
await this.client.del(key);
|
|
790
|
+
} else if (kept.length < events.length) {
|
|
791
|
+
const pipeline = this.client.pipeline();
|
|
792
|
+
pipeline.del(key);
|
|
793
|
+
for (const raw of kept) {
|
|
794
|
+
pipeline.rpush(key, raw);
|
|
795
|
+
}
|
|
796
|
+
await pipeline.exec();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
} while (cursor !== '0');
|
|
800
|
+
|
|
801
|
+
log(`Deleted ${totalDeleted} old job events`);
|
|
802
|
+
return totalDeleted;
|
|
648
803
|
}
|
|
649
804
|
|
|
650
805
|
async reclaimStuckJobs(maxProcessingTimeMinutes = 10): Promise<number> {
|
|
@@ -661,6 +816,230 @@ export class RedisBackend implements QueueBackend {
|
|
|
661
816
|
return Number(result);
|
|
662
817
|
}
|
|
663
818
|
|
|
819
|
+
// ── Wait / step-data support ────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Transition a job from 'processing' to 'waiting' status.
|
|
823
|
+
* Persists step data so the handler can resume from where it left off.
|
|
824
|
+
*
|
|
825
|
+
* @param jobId - The job to pause.
|
|
826
|
+
* @param options - Wait configuration including optional waitUntil date, token ID, and step data.
|
|
827
|
+
*/
|
|
828
|
+
async waitJob(
|
|
829
|
+
jobId: number,
|
|
830
|
+
options: {
|
|
831
|
+
waitUntil?: Date;
|
|
832
|
+
waitTokenId?: string;
|
|
833
|
+
stepData: Record<string, any>;
|
|
834
|
+
},
|
|
835
|
+
): Promise<void> {
|
|
836
|
+
const now = this.nowMs();
|
|
837
|
+
const waitUntilMs = options.waitUntil
|
|
838
|
+
? options.waitUntil.getTime().toString()
|
|
839
|
+
: 'null';
|
|
840
|
+
const waitTokenId = options.waitTokenId ?? 'null';
|
|
841
|
+
const stepDataJson = JSON.stringify(options.stepData);
|
|
842
|
+
|
|
843
|
+
const result = await this.client.eval(
|
|
844
|
+
WAIT_JOB_SCRIPT,
|
|
845
|
+
1,
|
|
846
|
+
this.prefix,
|
|
847
|
+
jobId,
|
|
848
|
+
waitUntilMs,
|
|
849
|
+
waitTokenId,
|
|
850
|
+
stepDataJson,
|
|
851
|
+
now,
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
if (Number(result) === 0) {
|
|
855
|
+
log(
|
|
856
|
+
`Job ${jobId} could not be set to waiting (may have been reclaimed or is no longer processing)`,
|
|
857
|
+
);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
await this.recordJobEvent(jobId, JobEventType.Waiting, {
|
|
862
|
+
waitUntil: options.waitUntil?.toISOString() ?? null,
|
|
863
|
+
waitTokenId: options.waitTokenId ?? null,
|
|
864
|
+
});
|
|
865
|
+
log(`Job ${jobId} set to waiting`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Persist step data for a job. Called after each ctx.run() step completes.
|
|
870
|
+
* Best-effort: does not throw to avoid killing the running handler.
|
|
871
|
+
*
|
|
872
|
+
* @param jobId - The job to update.
|
|
873
|
+
* @param stepData - The step data to persist.
|
|
874
|
+
*/
|
|
875
|
+
async updateStepData(
|
|
876
|
+
jobId: number,
|
|
877
|
+
stepData: Record<string, any>,
|
|
878
|
+
): Promise<void> {
|
|
879
|
+
try {
|
|
880
|
+
const now = this.nowMs();
|
|
881
|
+
await this.client.hset(
|
|
882
|
+
`${this.prefix}job:${jobId}`,
|
|
883
|
+
'stepData',
|
|
884
|
+
JSON.stringify(stepData),
|
|
885
|
+
'updatedAt',
|
|
886
|
+
now.toString(),
|
|
887
|
+
);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
log(`Error updating stepData for job ${jobId}: ${error}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Create a waitpoint token.
|
|
895
|
+
*
|
|
896
|
+
* @param jobId - The job ID to associate with the token (null if created outside a handler).
|
|
897
|
+
* @param options - Optional timeout string (e.g. '10m', '1h') and tags.
|
|
898
|
+
* @returns The created waitpoint with its unique ID.
|
|
899
|
+
*/
|
|
900
|
+
async createWaitpoint(
|
|
901
|
+
jobId: number | null,
|
|
902
|
+
options?: CreateTokenOptions,
|
|
903
|
+
): Promise<{ id: string }> {
|
|
904
|
+
const id = `wp_${randomUUID()}`;
|
|
905
|
+
const now = this.nowMs();
|
|
906
|
+
let timeoutAt: number | null = null;
|
|
907
|
+
|
|
908
|
+
if (options?.timeout) {
|
|
909
|
+
const ms = parseTimeoutString(options.timeout);
|
|
910
|
+
timeoutAt = now + ms;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const key = `${this.prefix}waitpoint:${id}`;
|
|
914
|
+
const fields: string[] = [
|
|
915
|
+
'id',
|
|
916
|
+
id,
|
|
917
|
+
'jobId',
|
|
918
|
+
jobId !== null ? jobId.toString() : 'null',
|
|
919
|
+
'status',
|
|
920
|
+
'waiting',
|
|
921
|
+
'output',
|
|
922
|
+
'null',
|
|
923
|
+
'timeoutAt',
|
|
924
|
+
timeoutAt !== null ? timeoutAt.toString() : 'null',
|
|
925
|
+
'createdAt',
|
|
926
|
+
now.toString(),
|
|
927
|
+
'completedAt',
|
|
928
|
+
'null',
|
|
929
|
+
'tags',
|
|
930
|
+
options?.tags ? JSON.stringify(options.tags) : 'null',
|
|
931
|
+
];
|
|
932
|
+
|
|
933
|
+
await (this.client as any).hmset(key, ...fields);
|
|
934
|
+
|
|
935
|
+
if (timeoutAt !== null) {
|
|
936
|
+
await this.client.zadd(`${this.prefix}waitpoint_timeout`, timeoutAt, id);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
log(`Created waitpoint ${id} for job ${jobId}`);
|
|
940
|
+
return { id };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Complete a waitpoint token and move the associated job back to 'pending'.
|
|
945
|
+
*
|
|
946
|
+
* @param tokenId - The waitpoint token ID to complete.
|
|
947
|
+
* @param data - Optional data to pass to the waiting handler.
|
|
948
|
+
*/
|
|
949
|
+
async completeWaitpoint(tokenId: string, data?: any): Promise<void> {
|
|
950
|
+
const now = this.nowMs();
|
|
951
|
+
const outputJson = data != null ? JSON.stringify(data) : 'null';
|
|
952
|
+
|
|
953
|
+
const result = await this.client.eval(
|
|
954
|
+
COMPLETE_WAITPOINT_SCRIPT,
|
|
955
|
+
1,
|
|
956
|
+
this.prefix,
|
|
957
|
+
tokenId,
|
|
958
|
+
outputJson,
|
|
959
|
+
now,
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
if (Number(result) === 0) {
|
|
963
|
+
log(`Waitpoint ${tokenId} not found or already completed`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
log(`Completed waitpoint ${tokenId}`);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Retrieve a waitpoint token by its ID.
|
|
972
|
+
*
|
|
973
|
+
* @param tokenId - The waitpoint token ID to look up.
|
|
974
|
+
* @returns The waitpoint record, or null if not found.
|
|
975
|
+
*/
|
|
976
|
+
async getWaitpoint(tokenId: string): Promise<WaitpointRecord | null> {
|
|
977
|
+
const data = await this.client.hgetall(
|
|
978
|
+
`${this.prefix}waitpoint:${tokenId}`,
|
|
979
|
+
);
|
|
980
|
+
if (!data || Object.keys(data).length === 0) return null;
|
|
981
|
+
|
|
982
|
+
const nullish = (v: string | undefined) =>
|
|
983
|
+
v === undefined || v === 'null' || v === '' ? null : v;
|
|
984
|
+
const numOrNull = (v: string | undefined): number | null => {
|
|
985
|
+
const n = nullish(v);
|
|
986
|
+
return n === null ? null : Number(n);
|
|
987
|
+
};
|
|
988
|
+
const dateOrNull = (v: string | undefined): Date | null => {
|
|
989
|
+
const n = numOrNull(v);
|
|
990
|
+
return n === null ? null : new Date(n);
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
let output: any = null;
|
|
994
|
+
if (data.output && data.output !== 'null') {
|
|
995
|
+
try {
|
|
996
|
+
output = JSON.parse(data.output);
|
|
997
|
+
} catch {
|
|
998
|
+
output = data.output;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
let tags: string[] | null = null;
|
|
1003
|
+
if (data.tags && data.tags !== 'null') {
|
|
1004
|
+
try {
|
|
1005
|
+
tags = JSON.parse(data.tags);
|
|
1006
|
+
} catch {
|
|
1007
|
+
/* ignore */
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
id: data.id,
|
|
1013
|
+
jobId: numOrNull(data.jobId),
|
|
1014
|
+
status: data.status as WaitpointRecord['status'],
|
|
1015
|
+
output,
|
|
1016
|
+
timeoutAt: dateOrNull(data.timeoutAt),
|
|
1017
|
+
createdAt: new Date(Number(data.createdAt)),
|
|
1018
|
+
completedAt: dateOrNull(data.completedAt),
|
|
1019
|
+
tags,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Expire timed-out waitpoint tokens and move their associated jobs back to 'pending'.
|
|
1025
|
+
*
|
|
1026
|
+
* @returns The number of tokens that were expired.
|
|
1027
|
+
*/
|
|
1028
|
+
async expireTimedOutWaitpoints(): Promise<number> {
|
|
1029
|
+
const now = this.nowMs();
|
|
1030
|
+
const result = (await this.client.eval(
|
|
1031
|
+
EXPIRE_TIMED_OUT_WAITPOINTS_SCRIPT,
|
|
1032
|
+
1,
|
|
1033
|
+
this.prefix,
|
|
1034
|
+
now,
|
|
1035
|
+
)) as number;
|
|
1036
|
+
const count = Number(result);
|
|
1037
|
+
if (count > 0) {
|
|
1038
|
+
log(`Expired ${count} timed-out waitpoints`);
|
|
1039
|
+
}
|
|
1040
|
+
return count;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
664
1043
|
// ── Internal helpers ──────────────────────────────────────────────────
|
|
665
1044
|
|
|
666
1045
|
async setPendingReasonForUnpickedJobs(
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createWaitpoint,
|
|
3
|
-
completeWaitpoint,
|
|
4
|
-
getWaitpoint,
|
|
5
|
-
expireTimedOutWaitpoints,
|
|
6
|
-
} from './queue.js';
|
|
7
1
|
import { createProcessor } from './processor.js';
|
|
8
2
|
import {
|
|
9
3
|
JobQueueConfig,
|
|
@@ -37,29 +31,18 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
37
31
|
setLogContext(config.verbose ?? false);
|
|
38
32
|
|
|
39
33
|
let backend: QueueBackend;
|
|
40
|
-
let pool: import('pg').Pool | undefined;
|
|
41
34
|
|
|
42
35
|
if (backendType === 'postgres') {
|
|
43
36
|
const pgConfig = config as PostgresJobQueueConfig;
|
|
44
|
-
pool = createPool(pgConfig.databaseConfig);
|
|
37
|
+
const pool = createPool(pgConfig.databaseConfig);
|
|
45
38
|
backend = new PostgresBackend(pool);
|
|
46
39
|
} else if (backendType === 'redis') {
|
|
47
40
|
const redisConfig = (config as RedisJobQueueConfig).redisConfig;
|
|
48
|
-
// RedisBackend constructor will throw if ioredis is not installed
|
|
49
41
|
backend = new RedisBackend(redisConfig);
|
|
50
42
|
} else {
|
|
51
43
|
throw new Error(`Unknown backend: ${backendType}`);
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
const requirePool = () => {
|
|
55
|
-
if (!pool) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
'Wait/Token features require the PostgreSQL backend. Configure with backend: "postgres" to use these features.',
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
return pool;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
46
|
/**
|
|
64
47
|
* Enqueue due cron jobs. Shared by the public API and the processor hook.
|
|
65
48
|
*/
|
|
@@ -158,9 +141,10 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
158
141
|
config.verbose ?? false,
|
|
159
142
|
),
|
|
160
143
|
retryJob: (jobId: number) => backend.retryJob(jobId),
|
|
161
|
-
cleanupOldJobs: (daysToKeep?: number) =>
|
|
162
|
-
|
|
163
|
-
|
|
144
|
+
cleanupOldJobs: (daysToKeep?: number, batchSize?: number) =>
|
|
145
|
+
backend.cleanupOldJobs(daysToKeep, batchSize),
|
|
146
|
+
cleanupOldJobEvents: (daysToKeep?: number, batchSize?: number) =>
|
|
147
|
+
backend.cleanupOldJobEvents(daysToKeep, batchSize),
|
|
164
148
|
cancelJob: withLogContext(
|
|
165
149
|
(jobId: number) => backend.cancelJob(jobId),
|
|
166
150
|
config.verbose ?? false,
|
|
@@ -232,23 +216,22 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
232
216
|
config.verbose ?? false,
|
|
233
217
|
),
|
|
234
218
|
|
|
235
|
-
// Wait / Token support (
|
|
219
|
+
// Wait / Token support (works with all backends)
|
|
236
220
|
createToken: withLogContext(
|
|
237
221
|
(options?: import('./types.js').CreateTokenOptions) =>
|
|
238
|
-
createWaitpoint(
|
|
222
|
+
backend.createWaitpoint(null, options),
|
|
239
223
|
config.verbose ?? false,
|
|
240
224
|
),
|
|
241
225
|
completeToken: withLogContext(
|
|
242
|
-
(tokenId: string, data?: any) =>
|
|
243
|
-
completeWaitpoint(requirePool(), tokenId, data),
|
|
226
|
+
(tokenId: string, data?: any) => backend.completeWaitpoint(tokenId, data),
|
|
244
227
|
config.verbose ?? false,
|
|
245
228
|
),
|
|
246
229
|
getToken: withLogContext(
|
|
247
|
-
(tokenId: string) => getWaitpoint(
|
|
230
|
+
(tokenId: string) => backend.getWaitpoint(tokenId),
|
|
248
231
|
config.verbose ?? false,
|
|
249
232
|
),
|
|
250
233
|
expireTimedOutTokens: withLogContext(
|
|
251
|
-
() => expireTimedOutWaitpoints(
|
|
234
|
+
() => backend.expireTimedOutWaitpoints(),
|
|
252
235
|
config.verbose ?? false,
|
|
253
236
|
),
|
|
254
237
|
|
|
@@ -339,12 +322,12 @@ export const initJobQueue = <PayloadMap = any>(
|
|
|
339
322
|
|
|
340
323
|
// Advanced access
|
|
341
324
|
getPool: () => {
|
|
342
|
-
if (
|
|
325
|
+
if (!(backend instanceof PostgresBackend)) {
|
|
343
326
|
throw new Error(
|
|
344
327
|
'getPool() is only available with the PostgreSQL backend.',
|
|
345
328
|
);
|
|
346
329
|
}
|
|
347
|
-
return
|
|
330
|
+
return backend.getPool();
|
|
348
331
|
},
|
|
349
332
|
getRedisClient: () => {
|
|
350
333
|
if (backendType !== 'redis') {
|