@nicnocquee/dataqueue 1.38.0 → 1.39.0-beta.20260322125514
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/ai/docs-content.json +11 -5
- package/ai/rules/advanced.md +27 -0
- package/ai/rules/basic.md +1 -1
- package/ai/skills/dataqueue-advanced/SKILL.md +40 -1
- package/ai/skills/dataqueue-core/SKILL.md +9 -0
- package/dist/index.cjs +563 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -2
- package/dist/index.d.ts +93 -2
- package/dist/index.js +558 -62
- package/dist/index.js.map +1 -1
- package/migrations/1781200000009_add_depends_on_to_job_queue.sql +10 -0
- package/package.json +1 -1
- package/src/backends/postgres.ts +254 -29
- package/src/backends/redis-scripts.ts +100 -4
- package/src/backends/redis.ts +194 -24
- package/src/index.ts +8 -0
- package/src/job-dependencies.test.ts +129 -0
- package/src/job-dependencies.ts +140 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { DatabaseClient } from './types.js';
|
|
2
|
+
import type { JobDependsOn } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a negative placeholder id for `addJobs` batch ordering: `-(index + 1)`.
|
|
6
|
+
* Resolves to the id of the job at `batchIndex` in the same batch after inserts.
|
|
7
|
+
*
|
|
8
|
+
* @param batchIndex - Zero-based index into the `addJobs` array.
|
|
9
|
+
*/
|
|
10
|
+
export function batchDepRef(batchIndex: number): number {
|
|
11
|
+
if (!Number.isInteger(batchIndex) || batchIndex < 0) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`batchDepRef: expected non-negative integer index, got ${batchIndex}`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return -(batchIndex + 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalizes optional `dependsOn`: empty arrays become undefined, ids de-duplicated.
|
|
21
|
+
*
|
|
22
|
+
* @param dep - Raw dependency options from the caller.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeDependsOn(dep?: JobDependsOn): {
|
|
25
|
+
jobIds: number[] | undefined;
|
|
26
|
+
tags: string[] | undefined;
|
|
27
|
+
} {
|
|
28
|
+
if (!dep) return { jobIds: undefined, tags: undefined };
|
|
29
|
+
const jobIds =
|
|
30
|
+
dep.jobIds && dep.jobIds.length > 0 ? [...new Set(dep.jobIds)] : undefined;
|
|
31
|
+
const tags =
|
|
32
|
+
dep.tags && dep.tags.length > 0 ? [...new Set(dep.tags)] : undefined;
|
|
33
|
+
return { jobIds, tags };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves batch-relative negative ids to real job ids after partial batch inserts.
|
|
38
|
+
*
|
|
39
|
+
* @param jobIds - May contain negative placeholders from {@link batchDepRef}.
|
|
40
|
+
* @param insertedIds - Ids inserted so far, index-aligned with the batch array prefix.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveDependsOnJobIdsForBatch(
|
|
43
|
+
jobIds: number[],
|
|
44
|
+
insertedIds: number[],
|
|
45
|
+
): number[] {
|
|
46
|
+
return jobIds.map((id) => {
|
|
47
|
+
if (id >= 0) return id;
|
|
48
|
+
const idx = -id - 1;
|
|
49
|
+
if (idx < 0 || idx >= insertedIds.length) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid batch-relative job id ${id}: index ${idx} out of range for ${insertedIds.length} inserted job(s)`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return insertedIds[idx]!;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true if `holderTags` contains every tag in `requiredTags` (set inclusion).
|
|
60
|
+
*
|
|
61
|
+
* @param holderTags - Tags on job X.
|
|
62
|
+
* @param requiredTags - `depends_on_tags` on dependent D.
|
|
63
|
+
*/
|
|
64
|
+
export function tagsAreSuperset(
|
|
65
|
+
holderTags: string[] | null | undefined,
|
|
66
|
+
requiredTags: string[] | null | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (!requiredTags || requiredTags.length === 0) return false;
|
|
69
|
+
if (!holderTags || holderTags.length === 0) return false;
|
|
70
|
+
const set = new Set(holderTags);
|
|
71
|
+
for (const t of requiredTags) {
|
|
72
|
+
if (!set.has(t)) return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Throws if inserting a job with `dependsOnJobIds` would create a cycle.
|
|
79
|
+
* Uses: jobs reachable downstream from `newJobId` must not include any prerequisite id
|
|
80
|
+
* (equivalently: a prerequisite must not lie in the downstream closure of `newJobId`).
|
|
81
|
+
*
|
|
82
|
+
* @param client - DB client (transaction).
|
|
83
|
+
* @param newJobId - Id of the row just inserted.
|
|
84
|
+
* @param dependsOnJobIds - Resolved positive prerequisite ids.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* Ensures every id in `jobIds` exists in `job_queue`.
|
|
88
|
+
*
|
|
89
|
+
* @param client - Database client.
|
|
90
|
+
* @param jobIds - Resolved positive job ids.
|
|
91
|
+
*/
|
|
92
|
+
export async function validatePrerequisiteJobIdsExist(
|
|
93
|
+
client: DatabaseClient,
|
|
94
|
+
jobIds: number[],
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (jobIds.length === 0) return;
|
|
97
|
+
const r = await client.query(
|
|
98
|
+
`SELECT COUNT(*)::int AS c FROM job_queue WHERE id = ANY($1::int[])`,
|
|
99
|
+
[jobIds],
|
|
100
|
+
);
|
|
101
|
+
const c = r.rows[0]?.c ?? 0;
|
|
102
|
+
if (c !== jobIds.length) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`dependsOn.jobIds: one or more job ids do not exist (${jobIds.join(', ')})`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function assertNoDependencyCycle(
|
|
110
|
+
client: DatabaseClient,
|
|
111
|
+
newJobId: number,
|
|
112
|
+
dependsOnJobIds: number[],
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
if (dependsOnJobIds.length === 0) return;
|
|
115
|
+
if (dependsOnJobIds.includes(newJobId)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Job ${newJobId} cannot depend on itself (dependsOn.jobIds)`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const result = await client.query(
|
|
121
|
+
`
|
|
122
|
+
WITH RECURSIVE downstream AS (
|
|
123
|
+
SELECT j.id
|
|
124
|
+
FROM job_queue j
|
|
125
|
+
WHERE j.depends_on_job_ids @> ARRAY[$1::integer]::integer[]
|
|
126
|
+
UNION
|
|
127
|
+
SELECT j.id
|
|
128
|
+
FROM job_queue j
|
|
129
|
+
INNER JOIN downstream d ON j.depends_on_job_ids @> ARRAY[d.id]::integer[]
|
|
130
|
+
)
|
|
131
|
+
SELECT 1 FROM downstream WHERE id = ANY($2::integer[]) LIMIT 1
|
|
132
|
+
`,
|
|
133
|
+
[newJobId, dependsOnJobIds],
|
|
134
|
+
);
|
|
135
|
+
if (result.rows.length > 0) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Adding job ${newJobId} would create a dependency cycle (dependsOn.jobIds)`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -42,6 +42,30 @@ export interface JobGroup {
|
|
|
42
42
|
tier?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Declares prerequisites for a job. Both dimensions use logical AND.
|
|
47
|
+
*
|
|
48
|
+
* - `jobIds`: The job will not run until every listed job is `completed`. If any
|
|
49
|
+
* prerequisite becomes `failed` or `cancelled`, pending dependents are cancelled (transitively).
|
|
50
|
+
* - `tags`: Active barrier — the job will not run while another job (not self) is
|
|
51
|
+
* `pending`, `processing`, or `waiting` whose `tags` are a superset of every tag listed here
|
|
52
|
+
* (Postgres `tags @> depends_on_tags`). If any such job becomes `failed` or `cancelled`,
|
|
53
|
+
* pending jobs that list these tags are cancelled (transitively).
|
|
54
|
+
*
|
|
55
|
+
* **`addJobs` batch references:** In a batch insert, a negative job id means a 0-based index
|
|
56
|
+
* into the same batch array: use {@link batchDepRef} (e.g. `batchDepRef(0)` for the first job).
|
|
57
|
+
* Single `addJob` calls must use positive database ids only.
|
|
58
|
+
*/
|
|
59
|
+
export interface JobDependsOn {
|
|
60
|
+
/** Prerequisite job ids (must all reach `completed`). */
|
|
61
|
+
jobIds?: number[];
|
|
62
|
+
/**
|
|
63
|
+
* Tag drain: wait until no active job (pending/processing/waiting) has all of these tags.
|
|
64
|
+
* Requires matching jobs to succeed (dependents are cancelled if a matching job fails or is cancelled).
|
|
65
|
+
*/
|
|
66
|
+
tags?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
export interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
|
|
46
70
|
jobType: T;
|
|
47
71
|
payload: PayloadMap[T];
|
|
@@ -149,6 +173,10 @@ export interface JobOptions<PayloadMap, T extends JobType<PayloadMap>> {
|
|
|
149
173
|
* globally limited by `group.id` across all workers/instances.
|
|
150
174
|
*/
|
|
151
175
|
group?: JobGroup;
|
|
176
|
+
/**
|
|
177
|
+
* Optional prerequisites (job ids and/or tag drain). See {@link JobDependsOn}.
|
|
178
|
+
*/
|
|
179
|
+
dependsOn?: JobDependsOn;
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
/**
|
|
@@ -309,6 +337,14 @@ export interface JobRecord<PayloadMap, T extends JobType<PayloadMap>> {
|
|
|
309
337
|
* Group tier for this job, if provided at enqueue time.
|
|
310
338
|
*/
|
|
311
339
|
groupTier?: string | null;
|
|
340
|
+
/**
|
|
341
|
+
* Prerequisite job ids persisted at enqueue time, if any.
|
|
342
|
+
*/
|
|
343
|
+
dependsOnJobIds?: number[] | null;
|
|
344
|
+
/**
|
|
345
|
+
* Tag drain prerequisites persisted at enqueue time, if any.
|
|
346
|
+
*/
|
|
347
|
+
dependsOnTags?: string[] | null;
|
|
312
348
|
}
|
|
313
349
|
|
|
314
350
|
/**
|