@tstdl/base 0.93.140 → 0.93.142
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/application/application.d.ts +1 -1
- package/application/application.js +1 -1
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/client/authentication.service.d.ts +1 -0
- package/authentication/client/authentication.service.js +3 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/circuit-breaker/circuit-breaker.d.ts +6 -4
- package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
- package/circuit-breaker/postgres/circuit-breaker.js +8 -5
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/module/modules/web-server.module.js +2 -3
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-api.test.js +5 -1
- package/notification/tests/notification-flow.test.js +8 -5
- package/orm/decorators.d.ts +22 -5
- package/orm/decorators.js +10 -1
- package/orm/server/bootstrap.d.ts +11 -0
- package/orm/server/bootstrap.js +31 -0
- package/orm/server/drizzle/schema-converter.d.ts +3 -1
- package/orm/server/drizzle/schema-converter.js +85 -56
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/extension.d.ts +14 -0
- package/orm/server/extension.js +27 -0
- package/orm/server/index.d.ts +3 -6
- package/orm/server/index.js +3 -6
- package/orm/server/migration.d.ts +18 -0
- package/orm/server/migration.js +58 -0
- package/orm/server/repository.d.ts +2 -1
- package/orm/server/repository.js +19 -9
- package/orm/server/transaction.d.ts +6 -10
- package/orm/server/transaction.js +25 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-extension.test.js +63 -0
- package/orm/tests/database-migration.test.js +83 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
- package/orm/tests/repository-compound-primary-key.test.js +234 -0
- package/orm/tests/schema-generation.test.d.ts +1 -0
- package/orm/tests/schema-generation.test.js +52 -5
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +5 -4
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/decorator-data.js +11 -12
- package/task-queue/README.md +2 -10
- package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +250 -89
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +6 -1
- package/task-queue/postgres/schemas.d.ts +15 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +18 -15
- package/task-queue/postgres/task-queue.js +797 -499
- package/task-queue/postgres/task.model.d.ts +20 -9
- package/task-queue/postgres/task.model.js +65 -39
- package/task-queue/task-context.d.ts +12 -7
- package/task-queue/task-context.js +8 -6
- package/task-queue/task-queue.d.ts +364 -43
- package/task-queue/task-queue.js +153 -41
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +395 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +150 -0
- package/task-queue/tests/dag.test.d.ts +1 -0
- package/task-queue/tests/dag.test.js +188 -0
- package/task-queue/tests/dependencies.test.js +165 -47
- package/task-queue/tests/enqueue-batch.test.d.ts +1 -0
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +94 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +114 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +39 -0
- package/task-queue/tests/queue.test.js +294 -49
- package/task-queue/tests/shutdown.test.d.ts +1 -0
- package/task-queue/tests/shutdown.test.js +41 -0
- package/task-queue/tests/transactions.test.d.ts +1 -0
- package/task-queue/tests/transactions.test.js +47 -0
- package/task-queue/tests/worker.test.js +63 -15
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/test5.js +5 -5
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +56 -29
- package/text/localization.service.js +2 -2
- package/utils/file-reader.js +1 -2
- package/utils/timing.d.ts +2 -2
- package/task-queue/postgres/drizzle/0000_simple_invisible_woman.sql +0 -74
- package/task-queue/tests/complex.test.js +0 -306
- package/task-queue/tests/extensive-dependencies.test.js +0 -234
- /package/{task-queue/tests/complex.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
- /package/{task-queue/tests/extensive-dependencies.test.d.ts → orm/tests/database-migration.test.d.ts} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
+
import { timeout } from '../../utils/timing.js';
|
|
5
|
+
describe('Extensive Task Queue Dependency Tests', () => {
|
|
6
|
+
let injector;
|
|
7
|
+
let queue;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
10
|
+
});
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
+
const queueName = `extensive-dep-queue-${crypto.randomUUID()}`;
|
|
14
|
+
queue = queueProvider.get(queueName, {
|
|
15
|
+
visibilityTimeout: 1000,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await queue.clear();
|
|
20
|
+
});
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await injector?.dispose();
|
|
23
|
+
});
|
|
24
|
+
async function completeTask(type) {
|
|
25
|
+
const dequeued = await queue.dequeue({ types: [type] });
|
|
26
|
+
if (!dequeued)
|
|
27
|
+
throw new Error(`Could not dequeue task of type ${type}`);
|
|
28
|
+
await queue.complete(dequeued);
|
|
29
|
+
}
|
|
30
|
+
it('should handle complex mixed chain: A -> (B -> D, C -> E) -> F', async () => {
|
|
31
|
+
// A is the root
|
|
32
|
+
const taskA = await queue.enqueue('A', {});
|
|
33
|
+
// B and C depend on A
|
|
34
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
35
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
|
|
36
|
+
// D depends on B, E depends on C
|
|
37
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id] });
|
|
38
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id] });
|
|
39
|
+
// F depends on D and E
|
|
40
|
+
const taskF = await queue.enqueue('F', {}, { scheduleAfter: [taskD.id, taskE.id] });
|
|
41
|
+
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
42
|
+
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
43
|
+
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
44
|
+
expect(taskD.status).toBe(TaskStatus.Waiting);
|
|
45
|
+
expect(taskE.status).toBe(TaskStatus.Waiting);
|
|
46
|
+
expect(taskF.status).toBe(TaskStatus.Waiting);
|
|
47
|
+
// 1. Complete A
|
|
48
|
+
await completeTask('A');
|
|
49
|
+
await queue.waitForTasks([taskB.id, taskC.id], { statuses: [TaskStatus.Pending] });
|
|
50
|
+
// 2. Complete B
|
|
51
|
+
await completeTask('B');
|
|
52
|
+
await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
|
|
53
|
+
// 3. Complete C
|
|
54
|
+
await completeTask('C');
|
|
55
|
+
await queue.waitForTasks([taskE.id], { statuses: [TaskStatus.Pending] });
|
|
56
|
+
// F should still be waiting
|
|
57
|
+
const uF = await queue.getTask(taskF.id);
|
|
58
|
+
expect(uF?.status).toBe(TaskStatus.Waiting);
|
|
59
|
+
// 4. Complete D
|
|
60
|
+
await completeTask('D');
|
|
61
|
+
// F still waiting for E
|
|
62
|
+
expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
|
|
63
|
+
// 5. Complete E
|
|
64
|
+
await completeTask('E');
|
|
65
|
+
await queue.waitForTasks([taskF.id], { statuses: [TaskStatus.Pending] });
|
|
66
|
+
});
|
|
67
|
+
it('should respect failFast = false (continue other branches)', async () => {
|
|
68
|
+
const taskA = await queue.enqueue('A', {});
|
|
69
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: false });
|
|
70
|
+
const taskC = await queue.enqueue('C', {}); // Independent
|
|
71
|
+
const dA = await queue.dequeue({ types: ['A'] });
|
|
72
|
+
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
73
|
+
// taskB should stay Waiting (default requiredStatus is Completed)
|
|
74
|
+
// If failFast is false, it should transition to Pending once the dependency is terminal, even if it failed.
|
|
75
|
+
await timeout(100);
|
|
76
|
+
const uB = await queue.getTask(taskB.id);
|
|
77
|
+
expect(uB?.status).toBe(TaskStatus.Pending);
|
|
78
|
+
const dC = await queue.dequeue({ types: ['C'] });
|
|
79
|
+
expect(dC?.id).toBe(taskC.id);
|
|
80
|
+
});
|
|
81
|
+
it('should handle priority aging', async () => {
|
|
82
|
+
const agingQueue = injector.resolve(TaskQueueProvider).get(`aging-${crypto.randomUUID()}`, {
|
|
83
|
+
priorityAgingInterval: 50,
|
|
84
|
+
priorityAgingStep: 10,
|
|
85
|
+
});
|
|
86
|
+
const t1 = await agingQueue.enqueue('low', {}, { priority: 2000 });
|
|
87
|
+
const t2 = await agingQueue.enqueue('high', {}, { priority: 1000 });
|
|
88
|
+
// Wait for aging to trigger
|
|
89
|
+
await timeout(100);
|
|
90
|
+
await agingQueue.maintenance();
|
|
91
|
+
const u1 = await agingQueue.getTask(t1.id);
|
|
92
|
+
const u2 = await agingQueue.getTask(t2.id);
|
|
93
|
+
expect(u1.priority).toBeLessThan(2000);
|
|
94
|
+
expect(u2.priority).toBeLessThan(1000);
|
|
95
|
+
});
|
|
96
|
+
it('should handle rate limiting', async () => {
|
|
97
|
+
const rateQueue = injector.resolve(TaskQueueProvider).get(`rate-${crypto.randomUUID()}`, {
|
|
98
|
+
rateLimit: 5,
|
|
99
|
+
rateInterval: 200,
|
|
100
|
+
});
|
|
101
|
+
// Limit is 5 per 200ms
|
|
102
|
+
for (let i = 0; i < 10; i++) {
|
|
103
|
+
await rateQueue.enqueue('task', { i });
|
|
104
|
+
}
|
|
105
|
+
const batch1 = await rateQueue.dequeueMany(10);
|
|
106
|
+
expect(batch1.length).toBe(5);
|
|
107
|
+
await timeout(500);
|
|
108
|
+
const batch2 = await rateQueue.dequeueMany(10);
|
|
109
|
+
expect(batch2.length).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
|
|
112
|
+
const taskA = await queue.enqueue('A', {});
|
|
113
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
114
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
|
|
115
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
|
|
116
|
+
await completeTask('A');
|
|
117
|
+
await queue.waitForTasks([taskB.id, taskC.id], { statuses: [TaskStatus.Pending] });
|
|
118
|
+
await completeTask('B');
|
|
119
|
+
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
120
|
+
await completeTask('C');
|
|
121
|
+
await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
|
|
122
|
+
});
|
|
123
|
+
it('should handle Deep Chain (A -> B -> C -> D)', async () => {
|
|
124
|
+
const taskA = await queue.enqueue('A', {});
|
|
125
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
126
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
|
|
127
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
|
|
128
|
+
await completeTask('A');
|
|
129
|
+
await queue.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending] });
|
|
130
|
+
await completeTask('B');
|
|
131
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
132
|
+
await completeTask('C');
|
|
133
|
+
await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
|
|
134
|
+
});
|
|
135
|
+
it('should handle wide fan-out (A -> [B1...B50] -> C)', async () => {
|
|
136
|
+
const taskA = await queue.enqueue('A', {});
|
|
137
|
+
const bTasks = await queue.enqueueMany(Array.from({ length: 50 }, (_, i) => ({ type: `B${i}`, data: {}, scheduleAfter: [taskA.id] })), { returnTasks: true });
|
|
138
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: bTasks.map(t => t.id) });
|
|
139
|
+
await completeTask('A');
|
|
140
|
+
for (let i = 0; i < 49; i++) {
|
|
141
|
+
await completeTask(`B${i}`);
|
|
142
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
143
|
+
}
|
|
144
|
+
await completeTask('B49');
|
|
145
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
146
|
+
});
|
|
147
|
+
it('should handle deep tree with branching and merging', async () => {
|
|
148
|
+
const taskA = await queue.enqueue('A', {});
|
|
149
|
+
const taskB1 = await queue.enqueue('B1', {}, { scheduleAfter: [taskA.id] });
|
|
150
|
+
const taskB2 = await queue.enqueue('B2', {}, { scheduleAfter: [taskA.id] });
|
|
151
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB1.id, taskB2.id] });
|
|
152
|
+
const taskD1 = await queue.enqueue('D1', {}, { scheduleAfter: [taskC.id] });
|
|
153
|
+
const taskD2 = await queue.enqueue('D2', {}, { scheduleAfter: [taskC.id] });
|
|
154
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskD1.id, taskD2.id] });
|
|
155
|
+
await completeTask('A');
|
|
156
|
+
await completeTask('B1');
|
|
157
|
+
await completeTask('B2');
|
|
158
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
159
|
+
await completeTask('C');
|
|
160
|
+
await queue.waitForTasks([taskD1.id], { statuses: [TaskStatus.Pending] });
|
|
161
|
+
await queue.waitForTasks([taskD2.id], { statuses: [TaskStatus.Pending] });
|
|
162
|
+
await completeTask('D1');
|
|
163
|
+
await completeTask('D2');
|
|
164
|
+
await queue.waitForTasks([taskE.id], { statuses: [TaskStatus.Pending] });
|
|
165
|
+
});
|
|
166
|
+
it('should handle fail-fast cascade', async () => {
|
|
167
|
+
const taskA = await queue.enqueue('A', {});
|
|
168
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: true });
|
|
169
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id], failFast: true });
|
|
170
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskA.id], failFast: true });
|
|
171
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id], failFast: true });
|
|
172
|
+
const dA = await queue.dequeue({ types: ['A'] });
|
|
173
|
+
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
174
|
+
await queue.waitForTasks([taskB.id, taskC.id, taskD.id, taskE.id], { statuses: [TaskStatus.Skipped] });
|
|
175
|
+
});
|
|
176
|
+
it('should NOT schedule if only one of two dependencies is met (AND logic)', async () => {
|
|
177
|
+
const taskA = await queue.enqueue('A', {});
|
|
178
|
+
const taskB = await queue.enqueue('B', {});
|
|
179
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id, taskB.id] });
|
|
180
|
+
await completeTask('A');
|
|
181
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
182
|
+
// Still waiting even after some time
|
|
183
|
+
await timeout(100);
|
|
184
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
185
|
+
await completeTask('B');
|
|
186
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
4
4
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
5
|
-
import { timeout } from '../../utils/timing.js';
|
|
6
5
|
describe('Queue Dependencies & Tree Tests', () => {
|
|
7
6
|
let injector;
|
|
8
7
|
let queue;
|
|
@@ -11,7 +10,7 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
11
10
|
});
|
|
12
11
|
beforeEach(() => {
|
|
13
12
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
14
|
-
const queueName = `dep-queue-${
|
|
13
|
+
const queueName = `dep-queue-${crypto.randomUUID()}`;
|
|
15
14
|
queue = queueProvider.get(queueName, {
|
|
16
15
|
visibilityTimeout: 1000,
|
|
17
16
|
});
|
|
@@ -22,46 +21,39 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
22
21
|
afterAll(async () => {
|
|
23
22
|
await injector?.dispose();
|
|
24
23
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
await queue.processPendingFanIn();
|
|
32
|
-
await timeout(50);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
describe('Dependencies (Fan-In)', () => {
|
|
36
|
-
it('should schedule a task only after dependency completes (completeAfterTags)', async () => {
|
|
37
|
-
// 1. Create a dependent task (Waiting)
|
|
24
|
+
describe('Dependencies (DAG)', () => {
|
|
25
|
+
it('should schedule a task only after dependency completes (completeAfter)', async () => {
|
|
26
|
+
// 1. Prereq
|
|
27
|
+
const prereq = await queue.enqueue('prereq', { val: 1 });
|
|
28
|
+
// 2. Dependent (Waiting)
|
|
38
29
|
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
39
|
-
|
|
30
|
+
completeAfter: [prereq.id],
|
|
40
31
|
});
|
|
41
|
-
expect(dependent.status).toBe(TaskStatus.
|
|
42
|
-
// 2. Create the prerequisite task
|
|
43
|
-
const prereq = await queue.enqueue('prereq', { val: 1 }, { tags: ['tag-a'] });
|
|
32
|
+
expect(dependent.status).toBe(TaskStatus.Pending);
|
|
44
33
|
// 3. Complete prereq
|
|
45
34
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
46
35
|
expect(dequeued?.id).toBe(prereq.id);
|
|
47
36
|
await queue.complete(dequeued);
|
|
48
|
-
|
|
37
|
+
// Complete dependent too so it reaches finalized state for waitForTasks
|
|
38
|
+
const dDependent = await queue.dequeue({ types: ['dependent'] });
|
|
39
|
+
expect(dDependent?.id).toBe(dependent.id);
|
|
40
|
+
await queue.complete(dDependent);
|
|
49
41
|
await queue.waitForTasks([dependent.id]);
|
|
50
42
|
const updatedDependent = await queue.getTask(dependent.id);
|
|
51
43
|
expect(updatedDependent?.status).toBe(TaskStatus.Completed);
|
|
52
44
|
});
|
|
53
|
-
it('should schedule a task to run after dependency completes (
|
|
54
|
-
// 1.
|
|
45
|
+
it('should schedule a task to run after dependency completes (scheduleAfter)', async () => {
|
|
46
|
+
// 1. Prereq
|
|
47
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
48
|
+
// 2. Dependent task
|
|
55
49
|
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
56
|
-
|
|
50
|
+
scheduleAfter: [prereq.id],
|
|
57
51
|
});
|
|
58
52
|
expect(dependent.status).toBe(TaskStatus.Waiting);
|
|
59
|
-
// 2. Prereq
|
|
60
|
-
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-b'] });
|
|
61
53
|
// 3. Complete prereq
|
|
62
54
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
63
55
|
await queue.complete(dequeued);
|
|
64
|
-
await
|
|
56
|
+
await queue.waitForTasks([dependent.id], { statuses: [TaskStatus.Pending] });
|
|
65
57
|
// 5. Dependent should be Pending (ready to run)
|
|
66
58
|
const updatedDependent = await queue.getTask(dependent.id);
|
|
67
59
|
expect(updatedDependent?.status).toBe(TaskStatus.Pending);
|
|
@@ -70,57 +62,164 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
70
62
|
expect(d2?.id).toBe(dependent.id);
|
|
71
63
|
});
|
|
72
64
|
it('should fail-fast if dependency fails', async () => {
|
|
65
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
73
66
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
74
|
-
|
|
67
|
+
scheduleAfter: [prereq.id],
|
|
75
68
|
failFast: true
|
|
76
69
|
});
|
|
77
|
-
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-fail'] });
|
|
78
70
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
79
71
|
// Fail fatally
|
|
80
72
|
await queue.fail(dequeued, new Error('boom'), { fatal: true });
|
|
81
|
-
await queue.processPendingFanIn();
|
|
82
73
|
await queue.waitForTasks([dependent.id]);
|
|
83
74
|
const updatedDependent = await queue.getTask(dependent.id);
|
|
84
|
-
expect(updatedDependent?.status).toBe(TaskStatus.
|
|
75
|
+
expect(updatedDependent?.status).toBe(TaskStatus.Skipped);
|
|
85
76
|
expect(updatedDependent?.error?.code).toBe('DependencyFailed');
|
|
86
77
|
});
|
|
87
|
-
it('should
|
|
88
|
-
|
|
78
|
+
it('should NOT overwrite terminal states during cancellation (failFast + complete)', async () => {
|
|
79
|
+
const dep = await queue.enqueue('dep', {});
|
|
80
|
+
// completeAfter allows 'main' to be Running while 'dep' is not finished
|
|
81
|
+
const main = await queue.enqueue('main', {}, { completeAfter: [dep.id], failFast: true });
|
|
82
|
+
const runningDep = await queue.dequeue();
|
|
83
|
+
expect(runningDep?.id).toBe(dep.id);
|
|
84
|
+
const runningMain = await queue.dequeue();
|
|
85
|
+
expect(runningMain?.id).toBe(main.id);
|
|
86
|
+
expect(runningMain?.status).toBe(TaskStatus.Running);
|
|
87
|
+
// Fail 'dep' -> this should make 'main' Skipped
|
|
88
|
+
await queue.fail(runningDep, new Error('Dependency failed'), { fatal: true });
|
|
89
|
+
const skippedMain = await queue.getTask(main.id);
|
|
90
|
+
expect(skippedMain?.status).toBe(TaskStatus.Skipped);
|
|
91
|
+
expect(skippedMain?.token).toBeNull();
|
|
92
|
+
// Call complete() on the old task reference (which still has the token)
|
|
93
|
+
await queue.complete(runningMain);
|
|
94
|
+
const finalMain = await queue.getTask(main.id);
|
|
95
|
+
expect(finalMain?.status).toBe(TaskStatus.Skipped);
|
|
96
|
+
});
|
|
97
|
+
it('should resolve dependencies across namespaces', async () => {
|
|
98
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
99
|
+
const queueA = queueProvider.get(`QueueA-${crypto.randomUUID()}`);
|
|
100
|
+
const queueB = queueProvider.get(`QueueB-${crypto.randomUUID()}`);
|
|
101
|
+
const taskA = await queueA.enqueue('test', { value: 'A' });
|
|
102
|
+
const taskB = await queueB.enqueue('test', { value: 'B' }, { scheduleAfter: [{ id: taskA.id }] });
|
|
103
|
+
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
104
|
+
const dequeuedA = await queueA.dequeue();
|
|
105
|
+
await queueA.complete(dequeuedA);
|
|
106
|
+
queueB.notify();
|
|
107
|
+
await queueB.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending], timeout: 10000 });
|
|
108
|
+
const updatedB = await queueB.getTask(taskB.id);
|
|
109
|
+
expect(updatedB?.status).toBe(TaskStatus.Pending);
|
|
110
|
+
await queueA.clear();
|
|
111
|
+
await queueB.clear();
|
|
112
|
+
});
|
|
113
|
+
it('should correctly skip dependent tasks across namespaces when failFast is triggered', async () => {
|
|
114
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
115
|
+
const queueA = queueProvider.get(`QueueA-${crypto.randomUUID()}`);
|
|
116
|
+
const queueB = queueProvider.get(`QueueB-${crypto.randomUUID()}`);
|
|
117
|
+
const taskA = await queueA.enqueue('test', { value: 'A' });
|
|
118
|
+
const taskB = await queueB.enqueue('test', { value: 'B' }, {
|
|
119
|
+
failFast: true,
|
|
120
|
+
scheduleAfter: [{ id: taskA.id }],
|
|
121
|
+
});
|
|
122
|
+
const dequeuedA = await queueA.dequeue();
|
|
123
|
+
await queueA.fail(dequeuedA, new Error('Failed'), { fatal: true });
|
|
124
|
+
queueB.notify();
|
|
125
|
+
await queueB.waitForTasks([taskB.id], { statuses: [TaskStatus.Skipped], timeout: 10000 });
|
|
126
|
+
const updatedB = await queueB.getTask(taskB.id);
|
|
127
|
+
expect(updatedB?.status).toBe(TaskStatus.Skipped);
|
|
128
|
+
await queueA.clear();
|
|
129
|
+
await queueB.clear();
|
|
130
|
+
});
|
|
131
|
+
it('should respect requiredStatuses', async () => {
|
|
132
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
133
|
+
// Wait for prereq to be Dead or Completed
|
|
89
134
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
90
|
-
|
|
91
|
-
dependencyJoinMode: DependencyJoinMode.Or
|
|
135
|
+
scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }]
|
|
92
136
|
});
|
|
93
|
-
//
|
|
94
|
-
await queue.
|
|
95
|
-
|
|
96
|
-
await queue.
|
|
97
|
-
await waitForStatus(dependent.id, TaskStatus.Pending);
|
|
137
|
+
// Fail fatally -> Dead
|
|
138
|
+
const d1 = await queue.dequeue({ types: ['prereq'] });
|
|
139
|
+
await queue.fail(d1, new Error('boom'), { fatal: true });
|
|
140
|
+
await queue.waitForTasks([dependent.id], { statuses: [TaskStatus.Pending] });
|
|
98
141
|
const updated = await queue.getTask(dependent.id);
|
|
99
142
|
expect(updated?.status).toBe(TaskStatus.Pending);
|
|
100
143
|
});
|
|
144
|
+
it('should schedule a task immediately if dependency is already completed (Bug 1)', async () => {
|
|
145
|
+
// 1. Enqueue and complete task A
|
|
146
|
+
const taskA = await queue.enqueue('taskA', {});
|
|
147
|
+
const dequeuedA = await queue.dequeue({ types: ['taskA'] });
|
|
148
|
+
await queue.complete(dequeuedA);
|
|
149
|
+
const finishedA = await queue.getTask(taskA.id);
|
|
150
|
+
expect(finishedA?.status).toBe(TaskStatus.Completed);
|
|
151
|
+
// 2. Enqueue task B with scheduleAfter: [A.id]
|
|
152
|
+
const taskB = await queue.enqueue('taskB', {}, {
|
|
153
|
+
scheduleAfter: [taskA.id],
|
|
154
|
+
});
|
|
155
|
+
// Wait for proactive resolution to trigger
|
|
156
|
+
await queue.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending] });
|
|
157
|
+
const updatedB = await queue.getTask(taskB.id);
|
|
158
|
+
expect(updatedB?.status).toBe(TaskStatus.Pending);
|
|
159
|
+
});
|
|
160
|
+
it('should transition to Pending even if duplicate dependencies are specified (Bug 2)', async () => {
|
|
161
|
+
// 1. Enqueue task A
|
|
162
|
+
const taskA = await queue.enqueue('taskA', {});
|
|
163
|
+
// 2. Enqueue task B with duplicate scheduleAfter: [A.id, A.id]
|
|
164
|
+
const taskB = await queue.enqueue('taskB', {}, {
|
|
165
|
+
scheduleAfter: [taskA.id, taskA.id],
|
|
166
|
+
});
|
|
167
|
+
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
168
|
+
// 3. Complete task A
|
|
169
|
+
const dequeuedA = await queue.dequeue({ types: ['taskA'] });
|
|
170
|
+
await queue.complete(dequeuedA);
|
|
171
|
+
await queue.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending] });
|
|
172
|
+
const updatedB = await queue.getTask(taskB.id);
|
|
173
|
+
expect(updatedB?.status).toBe(TaskStatus.Pending);
|
|
174
|
+
});
|
|
175
|
+
it('should NOT resurrect terminal tasks when calling rescheduleMany (Issue 1)', async () => {
|
|
176
|
+
const task = await queue.enqueue('test', { val: 1 });
|
|
177
|
+
const dequeued = await queue.dequeue();
|
|
178
|
+
await queue.complete(dequeued);
|
|
179
|
+
const completedTask = await queue.getTask(task.id);
|
|
180
|
+
expect(completedTask?.status).toBe(TaskStatus.Completed);
|
|
181
|
+
// Attempt to reschedule
|
|
182
|
+
await queue.rescheduleMany([task.id], Date.now());
|
|
183
|
+
const stillCompletedTask = await queue.getTask(task.id);
|
|
184
|
+
expect(stillCompletedTask?.status).toBe(TaskStatus.Completed);
|
|
185
|
+
expect(stillCompletedTask?.completeTimestamp).toBeDefined();
|
|
186
|
+
});
|
|
101
187
|
});
|
|
102
188
|
describe('Tree Operations & Cancellation', () => {
|
|
103
|
-
it('should retrieve task tree and cancel hierarchy', async () => {
|
|
189
|
+
it('should retrieve task tree and cancel hierarchy (multiple children)', async () => {
|
|
104
190
|
// Root
|
|
105
191
|
const root = await queue.enqueue('root', {});
|
|
106
|
-
//
|
|
107
|
-
const
|
|
192
|
+
// Children
|
|
193
|
+
const child1 = await queue.enqueue('child1', {}, { parentId: root.id });
|
|
194
|
+
const child2 = await queue.enqueue('child2', {}, { parentId: root.id });
|
|
108
195
|
// Grandchild
|
|
109
|
-
const grandchild = await queue.enqueue('grandchild', {}, { parentId:
|
|
196
|
+
const grandchild = await queue.enqueue('grandchild', {}, { parentId: child1.id });
|
|
110
197
|
// Get Tree
|
|
111
198
|
const tree = await queue.getTree(root.id);
|
|
112
|
-
expect(tree.length).toBe(
|
|
199
|
+
expect(tree.length).toBe(4);
|
|
113
200
|
expect(tree.map((t) => t.id)).toContain(grandchild.id);
|
|
201
|
+
expect(tree.map((t) => t.id)).toContain(child2.id);
|
|
114
202
|
// Cancel Root
|
|
115
203
|
await queue.cancel(root.id);
|
|
116
204
|
// Verify all cancelled
|
|
117
205
|
const tRoot = await queue.getTask(root.id);
|
|
118
|
-
const
|
|
206
|
+
const tChild1 = await queue.getTask(child1.id);
|
|
207
|
+
const tChild2 = await queue.getTask(child2.id);
|
|
119
208
|
const tGrand = await queue.getTask(grandchild.id);
|
|
120
209
|
expect(tRoot?.status).toBe(TaskStatus.Cancelled);
|
|
121
|
-
expect(
|
|
210
|
+
expect(tChild1?.status).toBe(TaskStatus.Cancelled);
|
|
211
|
+
expect(tChild2?.status).toBe(TaskStatus.Cancelled);
|
|
122
212
|
expect(tGrand?.status).toBe(TaskStatus.Cancelled);
|
|
123
213
|
});
|
|
214
|
+
it('should retrieve subtree via getTreeByQuery', async () => {
|
|
215
|
+
const root = await queue.enqueue('root', { val: 'root' });
|
|
216
|
+
const child = await queue.enqueue('child', { val: 'child' }, { parentId: root.id });
|
|
217
|
+
await queue.enqueue('other', { val: 'other' });
|
|
218
|
+
const tree = await queue.getTreeByQuery({ id: root.id });
|
|
219
|
+
expect(tree.length).toBe(2);
|
|
220
|
+
expect(tree.map(t => t.id)).toContain(root.id);
|
|
221
|
+
expect(tree.map(t => t.id)).toContain(child.id);
|
|
222
|
+
});
|
|
124
223
|
it('should cancel many by tags', async () => {
|
|
125
224
|
await queue.enqueue('t1', {}, { tags: ['group-a'] });
|
|
126
225
|
await queue.enqueue('t2', {}, { tags: ['group-a'] });
|
|
@@ -133,6 +232,25 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
133
232
|
const tasksB = await queue.getManyByTags('group-b');
|
|
134
233
|
expect(tasksB[0]?.status).toBe(TaskStatus.Pending);
|
|
135
234
|
});
|
|
235
|
+
it('should NOT resolve completed tasks as Cancelled when calling cancelMany (Issue 3)', async () => {
|
|
236
|
+
const taskA = await queue.enqueue('test', { name: 'A' });
|
|
237
|
+
const taskB = await queue.enqueue('test', { name: 'B' }, { scheduleAfter: [taskA.id], failFast: true });
|
|
238
|
+
const dequeuedA = await queue.dequeue();
|
|
239
|
+
await queue.complete(dequeuedA);
|
|
240
|
+
// B should have unresolvedScheduleDependencies = 0 and be Pending because A completed
|
|
241
|
+
const freshB = await queue.getTask(taskB.id);
|
|
242
|
+
expect(freshB?.unresolvedScheduleDependencies).toBe(0);
|
|
243
|
+
expect(freshB?.status).toBe(TaskStatus.Pending);
|
|
244
|
+
// Cancel A. Since A is Completed, it shouldn't be touched in DB.
|
|
245
|
+
// Importantly, it shouldn't trigger dependency resolution for B as 'Cancelled'.
|
|
246
|
+
// In the old code, this would have caused B to be transitioned to 'Skipped' because A was resolved as 'Cancelled' but B required 'Completed' (and failFast is true).
|
|
247
|
+
await queue.cancelMany([taskA.id]);
|
|
248
|
+
const stillFreshA = await queue.getTask(taskA.id);
|
|
249
|
+
expect(stillFreshA?.status).toBe(TaskStatus.Completed);
|
|
250
|
+
const stillFreshB = await queue.getTask(taskB.id);
|
|
251
|
+
expect(stillFreshB?.status).toBe(TaskStatus.Pending); // Should STILL be Pending
|
|
252
|
+
expect(stillFreshB?.unresolvedScheduleDependencies).toBe(0);
|
|
253
|
+
});
|
|
136
254
|
});
|
|
137
255
|
describe('Restart & Consumers', () => {
|
|
138
256
|
it('should restart a dead/cancelled task', async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { eq, inArray } from 'drizzle-orm';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Database } from '../../orm/server/index.js';
|
|
4
|
+
import { TaskQueueEnqueueBatch } from '../../task-queue/enqueue-batch.js';
|
|
5
|
+
import { TaskQueueProvider } from '../../task-queue/index.js';
|
|
6
|
+
import { taskDependency as taskDependencyTable } from '../../task-queue/postgres/schemas.js';
|
|
7
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
8
|
+
describe('TaskQueueEnqueueBatch', () => {
|
|
9
|
+
let injector;
|
|
10
|
+
let queue;
|
|
11
|
+
let database;
|
|
12
|
+
let queueProvider;
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
15
|
+
queueProvider = injector.resolve(TaskQueueProvider);
|
|
16
|
+
queue = queueProvider.get(`batch-queue-${crypto.randomUUID()}`);
|
|
17
|
+
database = injector.resolve(Database);
|
|
18
|
+
});
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await injector?.dispose();
|
|
21
|
+
});
|
|
22
|
+
it('should restore items if enqueueMany fails', async () => {
|
|
23
|
+
const batch = new TaskQueueEnqueueBatch(queue);
|
|
24
|
+
batch.add('t1', { val: 1 });
|
|
25
|
+
batch.add('t2', { val: 2 });
|
|
26
|
+
expect(batch.items.length).toBe(2);
|
|
27
|
+
// Mock enqueueMany to throw
|
|
28
|
+
vi.spyOn(queue, 'enqueueMany').mockRejectedValueOnce(new Error('db failure'));
|
|
29
|
+
await expect(batch.enqueue()).rejects.toThrow('db failure');
|
|
30
|
+
// Check items restored
|
|
31
|
+
expect(batch.items.length).toBe(2);
|
|
32
|
+
expect(batch.items[0].type).toBe('t1');
|
|
33
|
+
expect(batch.items[1].type).toBe('t2');
|
|
34
|
+
// Should work on second attempt (without mock)
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
const tasks = await batch.enqueue({ returnTasks: true });
|
|
37
|
+
expect(batch.items.length).toBe(0);
|
|
38
|
+
const t1 = await queue.getTask(tasks[0].id);
|
|
39
|
+
expect(t1).toBeDefined();
|
|
40
|
+
expect(t1?.type).toBe('t1');
|
|
41
|
+
});
|
|
42
|
+
it('should work correctly with queue.batch()', async () => {
|
|
43
|
+
const batch = queue.batch();
|
|
44
|
+
batch.add('b1', { x: 1 });
|
|
45
|
+
batch.add('b2', { x: 2 });
|
|
46
|
+
const tasks = await batch.enqueue({ returnTasks: true });
|
|
47
|
+
expect(tasks.length).toBe(2);
|
|
48
|
+
expect(batch.items.length).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
describe('enqueueMany Alignment and Dependencies', () => {
|
|
51
|
+
it('should correctly map dependencies when enqueuing a mix of tasks with and without idempotency keys', async () => {
|
|
52
|
+
const q = queueProvider.get(`q-${crypto.randomUUID()}`);
|
|
53
|
+
const items = [
|
|
54
|
+
{ type: 'test', data: { value: '1' } },
|
|
55
|
+
{ type: 'test', data: { value: '2' }, idempotencyKey: 'key-2' },
|
|
56
|
+
{ type: 'test', data: { value: '3' }, scheduleAfter: [0] },
|
|
57
|
+
{ type: 'test', data: { value: '4' }, idempotencyKey: 'key-4' },
|
|
58
|
+
{ type: 'test', data: { value: '5' }, scheduleAfter: [0] },
|
|
59
|
+
];
|
|
60
|
+
const tasks = await q.enqueueMany(items, { returnTasks: true });
|
|
61
|
+
expect(tasks).toHaveLength(5);
|
|
62
|
+
const task1Id = tasks[0].id;
|
|
63
|
+
const task3Id = tasks[2].id;
|
|
64
|
+
const task5Id = tasks[4].id;
|
|
65
|
+
const deps = await database
|
|
66
|
+
.select()
|
|
67
|
+
.from(taskDependencyTable)
|
|
68
|
+
.where(inArray(taskDependencyTable.taskId, [task3Id, task5Id]));
|
|
69
|
+
expect(deps).toHaveLength(2);
|
|
70
|
+
expect(deps.every(d => d.dependencyTaskId === task1Id)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('should correctly map dependencies when batch-enqueuing with pre-existing idempotency keys', async () => {
|
|
73
|
+
const q = queueProvider.get(`q-${crypto.randomUUID()}`);
|
|
74
|
+
const existingTask = await q.enqueue('test', { value: 'existing' }, { idempotencyKey: 'shared-key' });
|
|
75
|
+
const items = [
|
|
76
|
+
{ type: 'test', data: { value: 'new' } },
|
|
77
|
+
{ type: 'test', data: { value: 'duplicate' }, idempotencyKey: 'shared-key' },
|
|
78
|
+
{ type: 'test', data: { value: 'dependent' }, scheduleAfter: [1] },
|
|
79
|
+
];
|
|
80
|
+
const tasks = await q.enqueueMany(items, { returnTasks: true, replace: false });
|
|
81
|
+
expect(tasks[1].id).toBe(existingTask.id);
|
|
82
|
+
const dependentTaskId = tasks[2].id;
|
|
83
|
+
const deps = await database
|
|
84
|
+
.select()
|
|
85
|
+
.from(taskDependencyTable)
|
|
86
|
+
.where(eq(taskDependencyTable.taskId, dependentTaskId));
|
|
87
|
+
expect(deps).toHaveLength(1);
|
|
88
|
+
expect(deps[0].dependencyTaskId).toBe(existingTask.id);
|
|
89
|
+
});
|
|
90
|
+
it('should accurately increment parent unresolvedCompleteDependencies across multiple namespaces in a single batch', async () => {
|
|
91
|
+
const queueA = queueProvider.get(`QueueA-${crypto.randomUUID()}`);
|
|
92
|
+
const queueB = queueProvider.get(`QueueB-${crypto.randomUUID()}`);
|
|
93
|
+
const queueC = queueProvider.get(`QueueC-${crypto.randomUUID()}`);
|
|
94
|
+
const parentA = await queueA.enqueue('test', { value: 'parentA' });
|
|
95
|
+
const parentB = await queueB.enqueue('test', { value: 'parentB' });
|
|
96
|
+
await queueC.enqueueMany([
|
|
97
|
+
{ type: 'test', data: { value: 'child1' }, parentId: parentA.id },
|
|
98
|
+
{ type: 'test', data: { value: 'child2' }, parentId: parentA.id },
|
|
99
|
+
{ type: 'test', data: { value: 'child3' }, parentId: parentB.id },
|
|
100
|
+
]);
|
|
101
|
+
const upA = await queueA.getTask(parentA.id);
|
|
102
|
+
const upB = await queueB.getTask(parentB.id);
|
|
103
|
+
expect(upA?.unresolvedCompleteDependencies).toBe(2);
|
|
104
|
+
expect(upB?.unresolvedCompleteDependencies).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
it('should correctly map dependencies when using object with index', async () => {
|
|
107
|
+
const q = queueProvider.get(`q-${crypto.randomUUID()}`);
|
|
108
|
+
const items = [
|
|
109
|
+
{ type: 'test', data: { value: '1' } },
|
|
110
|
+
{ type: 'test', data: { value: '2' }, scheduleAfter: [0] },
|
|
111
|
+
{ type: 'test', data: { value: '3' }, scheduleAfter: [{ index: 0 }] },
|
|
112
|
+
];
|
|
113
|
+
const tasks = await q.enqueueMany(items, { returnTasks: true });
|
|
114
|
+
const task1Id = tasks[0].id;
|
|
115
|
+
const task2Id = tasks[1].id;
|
|
116
|
+
const task3Id = tasks[2].id;
|
|
117
|
+
const deps = await database
|
|
118
|
+
.select()
|
|
119
|
+
.from(taskDependencyTable)
|
|
120
|
+
.where(inArray(taskDependencyTable.taskId, [task2Id, task3Id]));
|
|
121
|
+
expect(deps).toHaveLength(2);
|
|
122
|
+
expect(deps.every(d => d.dependencyTaskId === task1Id)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|