@tstdl/base 0.93.140 → 0.93.141
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/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/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- 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-flow.test.js +2 -2
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +4 -3
- 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 -9
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- 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 +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +2 -12
- package/task-queue/postgres/task-queue.js +431 -354
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +7 -7
- package/task-queue/task-queue.d.ts +36 -19
- package/task-queue/task-queue.js +18 -10
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +44 -228
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +26 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +33 -24
- package/task-queue/tests/worker.test.js +20 -5
- 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 +54 -29
- package/text/localization.service.js +2 -2
- package/utils/file-reader.js +1 -2
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
import { timeout } from '../../utils/timing.js';
|
|
6
6
|
describe('Queue Dependencies & Tree Tests', () => {
|
|
@@ -28,36 +28,38 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
28
28
|
if (task?.status == status) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
await queue.processPendingFanIn();
|
|
32
31
|
await timeout(50);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
|
-
describe('Dependencies (
|
|
36
|
-
it('should schedule a task only after dependency completes (
|
|
37
|
-
// 1.
|
|
34
|
+
describe('Dependencies (DAG)', () => {
|
|
35
|
+
it('should schedule a task only after dependency completes (completeAfter)', async () => {
|
|
36
|
+
// 1. Prereq
|
|
37
|
+
const prereq = await queue.enqueue('prereq', { val: 1 });
|
|
38
|
+
// 2. Dependent (Waiting)
|
|
38
39
|
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
39
|
-
|
|
40
|
+
completeAfter: [prereq.id],
|
|
40
41
|
});
|
|
41
|
-
expect(dependent.status).toBe(TaskStatus.
|
|
42
|
-
// 2. Create the prerequisite task
|
|
43
|
-
const prereq = await queue.enqueue('prereq', { val: 1 }, { tags: ['tag-a'] });
|
|
42
|
+
expect(dependent.status).toBe(TaskStatus.Pending);
|
|
44
43
|
// 3. Complete prereq
|
|
45
44
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
46
45
|
expect(dequeued?.id).toBe(prereq.id);
|
|
47
46
|
await queue.complete(dequeued);
|
|
48
|
-
|
|
47
|
+
// Complete dependent too so it reaches finalized state for waitForTasks
|
|
48
|
+
const dDependent = await queue.dequeue({ types: ['dependent'] });
|
|
49
|
+
expect(dDependent?.id).toBe(dependent.id);
|
|
50
|
+
await queue.complete(dDependent);
|
|
49
51
|
await queue.waitForTasks([dependent.id]);
|
|
50
52
|
const updatedDependent = await queue.getTask(dependent.id);
|
|
51
53
|
expect(updatedDependent?.status).toBe(TaskStatus.Completed);
|
|
52
54
|
});
|
|
53
|
-
it('should schedule a task to run after dependency completes (
|
|
54
|
-
// 1.
|
|
55
|
+
it('should schedule a task to run after dependency completes (scheduleAfter)', async () => {
|
|
56
|
+
// 1. Prereq
|
|
57
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
58
|
+
// 2. Dependent task
|
|
55
59
|
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
56
|
-
|
|
60
|
+
scheduleAfter: [prereq.id],
|
|
57
61
|
});
|
|
58
62
|
expect(dependent.status).toBe(TaskStatus.Waiting);
|
|
59
|
-
// 2. Prereq
|
|
60
|
-
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-b'] });
|
|
61
63
|
// 3. Complete prereq
|
|
62
64
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
63
65
|
await queue.complete(dequeued);
|
|
@@ -70,30 +72,28 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
70
72
|
expect(d2?.id).toBe(dependent.id);
|
|
71
73
|
});
|
|
72
74
|
it('should fail-fast if dependency fails', async () => {
|
|
75
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
73
76
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
74
|
-
|
|
77
|
+
scheduleAfter: [prereq.id],
|
|
75
78
|
failFast: true
|
|
76
79
|
});
|
|
77
|
-
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-fail'] });
|
|
78
80
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
79
81
|
// Fail fatally
|
|
80
82
|
await queue.fail(dequeued, new Error('boom'), { fatal: true });
|
|
81
|
-
await queue.processPendingFanIn();
|
|
82
83
|
await queue.waitForTasks([dependent.id]);
|
|
83
84
|
const updatedDependent = await queue.getTask(dependent.id);
|
|
84
85
|
expect(updatedDependent?.status).toBe(TaskStatus.Dead);
|
|
85
86
|
expect(updatedDependent?.error?.code).toBe('DependencyFailed');
|
|
86
87
|
});
|
|
87
|
-
it('should respect
|
|
88
|
-
|
|
88
|
+
it('should respect requiredStatuses', async () => {
|
|
89
|
+
const prereq = await queue.enqueue('prereq', {});
|
|
90
|
+
// Wait for prereq to be Dead or Completed
|
|
89
91
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
90
|
-
|
|
91
|
-
dependencyJoinMode: DependencyJoinMode.Or
|
|
92
|
+
scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }]
|
|
92
93
|
});
|
|
93
|
-
//
|
|
94
|
-
await queue.
|
|
95
|
-
|
|
96
|
-
await queue.complete(d1);
|
|
94
|
+
// Fail fatally -> Dead
|
|
95
|
+
const d1 = await queue.dequeue({ types: ['prereq'] });
|
|
96
|
+
await queue.fail(d1, new Error('boom'), { fatal: true });
|
|
97
97
|
await waitForStatus(dependent.id, TaskStatus.Pending);
|
|
98
98
|
const updated = await queue.getTask(dependent.id);
|
|
99
99
|
expect(updated?.status).toBe(TaskStatus.Pending);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
3
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
4
|
import { timeout } from '../../utils/timing.js';
|
|
5
5
|
describe('Extensive Task Queue Dependency Tests', () => {
|
|
@@ -26,7 +26,6 @@ describe('Extensive Task Queue Dependency Tests', () => {
|
|
|
26
26
|
const task = await queue.getTask(id);
|
|
27
27
|
if (task?.status === status)
|
|
28
28
|
return;
|
|
29
|
-
await queue.processPendingFanIn();
|
|
30
29
|
await timeout(50);
|
|
31
30
|
}
|
|
32
31
|
const finalTask = await queue.getTask(id);
|
|
@@ -37,19 +36,18 @@ describe('Extensive Task Queue Dependency Tests', () => {
|
|
|
37
36
|
if (!dequeued)
|
|
38
37
|
throw new Error(`Could not dequeue task of type ${type}`);
|
|
39
38
|
await queue.complete(dequeued);
|
|
40
|
-
await queue.processPendingFanIn();
|
|
41
39
|
}
|
|
42
40
|
it('should handle complex mixed chain: A -> (B -> D, C -> E) -> F', async () => {
|
|
43
|
-
// F depends on D and E
|
|
44
|
-
const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['mc-d', 'mc-e'] });
|
|
45
|
-
// D depends on B, E depends on C
|
|
46
|
-
const taskD = await queue.enqueue('D', {}, { tags: ['mc-d'], scheduleAfterTags: ['mc-b'] });
|
|
47
|
-
const taskE = await queue.enqueue('E', {}, { tags: ['mc-e'], scheduleAfterTags: ['mc-c'] });
|
|
48
|
-
// B and C depend on A
|
|
49
|
-
const taskB = await queue.enqueue('B', {}, { tags: ['mc-b'], scheduleAfterTags: ['mc-a'] });
|
|
50
|
-
const taskC = await queue.enqueue('C', {}, { tags: ['mc-c'], scheduleAfterTags: ['mc-a'] });
|
|
51
41
|
// A is the root
|
|
52
|
-
const taskA = await queue.enqueue('A', {}
|
|
42
|
+
const taskA = await queue.enqueue('A', {});
|
|
43
|
+
// B and C depend on A
|
|
44
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
45
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
|
|
46
|
+
// D depends on B, E depends on C
|
|
47
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id] });
|
|
48
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id] });
|
|
49
|
+
// F depends on D and E
|
|
50
|
+
const taskF = await queue.enqueue('F', {}, { scheduleAfter: [taskD.id, taskE.id] });
|
|
53
51
|
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
54
52
|
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
55
53
|
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
@@ -77,158 +75,85 @@ describe('Extensive Task Queue Dependency Tests', () => {
|
|
|
77
75
|
await completeTask('E');
|
|
78
76
|
await waitForStatus(taskF.id, TaskStatus.Pending);
|
|
79
77
|
});
|
|
80
|
-
it('should handle
|
|
81
|
-
const
|
|
82
|
-
const taskB = await queue.enqueue('B', {}, {
|
|
83
|
-
const taskC = await queue.enqueue('C', {}, {
|
|
84
|
-
const
|
|
85
|
-
// Initial check
|
|
86
|
-
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
87
|
-
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
88
|
-
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
89
|
-
expect(taskD.status).toBe(TaskStatus.Waiting);
|
|
90
|
-
// Complete A
|
|
78
|
+
it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
|
|
79
|
+
const taskA = await queue.enqueue('A', {});
|
|
80
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
81
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
|
|
82
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
|
|
91
83
|
await completeTask('A');
|
|
92
84
|
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
93
85
|
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
94
|
-
|
|
95
|
-
const dB = await queue.dequeue({ types: ['B'] });
|
|
96
|
-
const dC = await queue.dequeue({ types: ['C'] });
|
|
97
|
-
expect(dB).toBeDefined();
|
|
98
|
-
expect(dC).toBeDefined();
|
|
99
|
-
// D still waiting
|
|
100
|
-
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
101
|
-
// Complete B
|
|
102
|
-
await queue.complete(dB);
|
|
103
|
-
await queue.processPendingFanIn();
|
|
86
|
+
await completeTask('B');
|
|
104
87
|
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
105
|
-
|
|
106
|
-
await queue.complete(dC);
|
|
88
|
+
await completeTask('C');
|
|
107
89
|
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
108
90
|
});
|
|
109
|
-
it('should
|
|
110
|
-
const
|
|
111
|
-
const taskB = await queue.enqueue('B', {}, {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
expect(dA?.id).toBe(taskA.id);
|
|
121
|
-
await queue.complete(dA);
|
|
122
|
-
await queue.processPendingFanIn();
|
|
123
|
-
// Now B should be available, but C still not
|
|
124
|
-
const dC_2 = await queue.dequeue({ types: ['C'] });
|
|
125
|
-
expect(dC_2).toBeUndefined();
|
|
126
|
-
const dB_2 = await queue.dequeue({ types: ['B'] });
|
|
127
|
-
expect(dB_2?.id).toBe(taskB.id);
|
|
128
|
-
await queue.complete(dB_2);
|
|
129
|
-
await queue.processPendingFanIn();
|
|
130
|
-
// Now C should be available
|
|
131
|
-
const dC_3 = await queue.dequeue({ types: ['C'] });
|
|
132
|
-
expect(dC_3?.id).toBe(taskC.id);
|
|
133
|
-
await queue.complete(dC_3);
|
|
134
|
-
});
|
|
135
|
-
it('should handle large fan-in: (T1, T2, T3, T4, T5) -> Result', async () => {
|
|
136
|
-
const tags = ['t1', 't2', 't3', 't4', 't5'];
|
|
137
|
-
const taskResult = await queue.enqueue('Result', {}, { scheduleAfterTags: tags });
|
|
138
|
-
for (const tag of tags) {
|
|
139
|
-
await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
|
|
140
|
-
}
|
|
141
|
-
// Complete all but one
|
|
142
|
-
for (let i = 0; i < 4; i++) {
|
|
143
|
-
await completeTask(`Task-${tags[i]}`);
|
|
144
|
-
expect((await queue.getTask(taskResult.id))?.status).toBe(TaskStatus.Waiting);
|
|
145
|
-
}
|
|
146
|
-
// Complete the last one
|
|
147
|
-
await completeTask(`Task-${tags[4]}`);
|
|
148
|
-
await waitForStatus(taskResult.id, TaskStatus.Pending);
|
|
91
|
+
it('should handle Deep Chain (A -> B -> C -> D)', async () => {
|
|
92
|
+
const taskA = await queue.enqueue('A', {});
|
|
93
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
94
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
|
|
95
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
|
|
96
|
+
await completeTask('A');
|
|
97
|
+
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
98
|
+
await completeTask('B');
|
|
99
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
100
|
+
await completeTask('C');
|
|
101
|
+
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
149
102
|
});
|
|
150
|
-
it('should handle
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await queue.
|
|
103
|
+
it('should handle wide fan-out (A -> [B1...B50] -> C)', async () => {
|
|
104
|
+
const taskA = await queue.enqueue('A', {});
|
|
105
|
+
const bTasks = await queue.enqueueMany(Array.from({ length: 50 }, (_, i) => ({ type: `B${i}`, data: {}, scheduleAfter: [taskA.id] })), { returnTasks: true });
|
|
106
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: bTasks.map(t => t.id) });
|
|
107
|
+
await completeTask('A');
|
|
108
|
+
for (let i = 0; i < 49; i++) {
|
|
109
|
+
await completeTask(`B${i}`);
|
|
110
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
158
111
|
}
|
|
159
|
-
|
|
160
|
-
await
|
|
161
|
-
await waitForStatus(taskResult.id, TaskStatus.Pending);
|
|
112
|
+
await completeTask('B49');
|
|
113
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
162
114
|
});
|
|
163
|
-
it('should handle
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const taskC = await queue.enqueue('C', {}, {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
// Step by step completion
|
|
115
|
+
it('should handle deep tree with branching and merging', async () => {
|
|
116
|
+
const taskA = await queue.enqueue('A', {});
|
|
117
|
+
const taskB1 = await queue.enqueue('B1', {}, { scheduleAfter: [taskA.id] });
|
|
118
|
+
const taskB2 = await queue.enqueue('B2', {}, { scheduleAfter: [taskA.id] });
|
|
119
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB1.id, taskB2.id] });
|
|
120
|
+
const taskD1 = await queue.enqueue('D1', {}, { scheduleAfter: [taskC.id] });
|
|
121
|
+
const taskD2 = await queue.enqueue('D2', {}, { scheduleAfter: [taskC.id] });
|
|
122
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskD1.id, taskD2.id] });
|
|
172
123
|
await completeTask('A');
|
|
173
|
-
await waitForStatus(taskB1.id, TaskStatus.Pending);
|
|
174
|
-
await waitForStatus(taskB2.id, TaskStatus.Pending);
|
|
175
124
|
await completeTask('B1');
|
|
176
|
-
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
177
125
|
await completeTask('B2');
|
|
178
126
|
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
179
127
|
await completeTask('C');
|
|
180
128
|
await waitForStatus(taskD1.id, TaskStatus.Pending);
|
|
181
129
|
await waitForStatus(taskD2.id, TaskStatus.Pending);
|
|
182
130
|
await completeTask('D1');
|
|
183
|
-
expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
|
|
184
131
|
await completeTask('D2');
|
|
185
132
|
await waitForStatus(taskE.id, TaskStatus.Pending);
|
|
186
133
|
});
|
|
187
|
-
it('should fail-fast
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
197
|
-
await completeTask('A');
|
|
198
|
-
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
199
|
-
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
200
|
-
// Fail B fatally
|
|
201
|
-
const dB = await queue.dequeue({ types: ['B'] });
|
|
202
|
-
await queue.fail(dB, new Error('fatal B'), { fatal: true });
|
|
203
|
-
await queue.processPendingFanIn();
|
|
204
|
-
// B failed fatally -> C should die -> F should die
|
|
134
|
+
it('should handle fail-fast cascade', async () => {
|
|
135
|
+
const taskA = await queue.enqueue('A', {});
|
|
136
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: true });
|
|
137
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id], failFast: true });
|
|
138
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskA.id], failFast: true });
|
|
139
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id], failFast: true });
|
|
140
|
+
const dA = await queue.dequeue({ types: ['A'] });
|
|
141
|
+
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
142
|
+
await waitForStatus(taskB.id, TaskStatus.Dead);
|
|
205
143
|
await waitForStatus(taskC.id, TaskStatus.Dead);
|
|
206
|
-
await waitForStatus(
|
|
207
|
-
|
|
208
|
-
const uD = await queue.getTask(taskD.id);
|
|
209
|
-
expect(uD?.status).toBe(TaskStatus.Pending);
|
|
144
|
+
await waitForStatus(taskD.id, TaskStatus.Dead);
|
|
145
|
+
await waitForStatus(taskE.id, TaskStatus.Dead);
|
|
210
146
|
});
|
|
211
|
-
it('should
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['C', 'D'] });
|
|
216
|
-
const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'D'] });
|
|
217
|
-
const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A', 'B'] });
|
|
218
|
-
const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A', 'B'] });
|
|
219
|
-
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
220
|
-
const taskB = await queue.enqueue('B', {}, { tags: ['B'] });
|
|
147
|
+
it('should NOT schedule if only one of two dependencies is met (AND logic)', async () => {
|
|
148
|
+
const taskA = await queue.enqueue('A', {});
|
|
149
|
+
const taskB = await queue.enqueue('B', {});
|
|
150
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id, taskB.id] });
|
|
221
151
|
await completeTask('A');
|
|
222
152
|
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
223
|
-
|
|
153
|
+
// Still waiting even after some time
|
|
154
|
+
await timeout(100);
|
|
155
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
224
156
|
await completeTask('B');
|
|
225
157
|
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
226
|
-
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
227
|
-
await completeTask('C');
|
|
228
|
-
expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
|
|
229
|
-
expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
|
|
230
|
-
await completeTask('D');
|
|
231
|
-
await waitForStatus(taskE.id, TaskStatus.Pending);
|
|
232
|
-
await waitForStatus(taskF.id, TaskStatus.Pending);
|
|
233
158
|
});
|
|
234
159
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
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('Fan-Out Spawning', () => {
|
|
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 = `fan-out-queue-${Date.now()}-${Math.random()}`;
|
|
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
|
+
it('should transition parent to Waiting when spawning children during execution', async () => {
|
|
25
|
+
const parent = await queue.enqueue('parent', {});
|
|
26
|
+
const dParent = await queue.dequeue();
|
|
27
|
+
expect(dParent?.id).toBe(parent.id);
|
|
28
|
+
// Parent spawns a child
|
|
29
|
+
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id }], { returnTasks: true });
|
|
30
|
+
// Complete parent
|
|
31
|
+
await queue.complete(dParent);
|
|
32
|
+
// Parent should be Waiting for child
|
|
33
|
+
const uParent = await queue.getTask(parent.id);
|
|
34
|
+
expect(uParent?.status).toBe(TaskStatus.WaitingChildren);
|
|
35
|
+
expect(uParent?.unresolvedCompleteDependencies).toBe(1);
|
|
36
|
+
// Complete child
|
|
37
|
+
const dChild = await queue.dequeue();
|
|
38
|
+
await queue.complete(dChild);
|
|
39
|
+
// Parent should now be Completed
|
|
40
|
+
const fParent = await queue.getTask(parent.id);
|
|
41
|
+
expect(fParent?.status).toBe(TaskStatus.Completed);
|
|
42
|
+
});
|
|
43
|
+
it('should NOT transition parent to Waiting if waitForCompletion is false', async () => {
|
|
44
|
+
const parent = await queue.enqueue('parent', {});
|
|
45
|
+
const dParent = await queue.dequeue();
|
|
46
|
+
// Spawn child with waitForCompletion: false
|
|
47
|
+
await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, waitForCompletion: false }], { returnTasks: true });
|
|
48
|
+
await queue.complete(dParent);
|
|
49
|
+
const uParent = await queue.getTask(parent.id);
|
|
50
|
+
expect(uParent?.status).toBe(TaskStatus.Completed); // Finished immediately
|
|
51
|
+
expect(uParent?.unresolvedCompleteDependencies).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
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('Idempotent Replacement', () => {
|
|
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 = `idempotent-queue-${Date.now()}-${Math.random()}`;
|
|
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
|
+
it('should NOT be affected by "ghost" dependencies after replacement', async () => {
|
|
25
|
+
const depA = await queue.enqueue('depA', {});
|
|
26
|
+
const depB = await queue.enqueue('depB', {});
|
|
27
|
+
const depC = await queue.enqueue('depC', {});
|
|
28
|
+
const idempotencyKey = 'my-idempotent-task';
|
|
29
|
+
// 1. Initial enqueue with A and B
|
|
30
|
+
const taskV1 = await queue.enqueue('main', { version: 1 }, {
|
|
31
|
+
idempotencyKey,
|
|
32
|
+
scheduleAfter: [depA.id, depB.id],
|
|
33
|
+
});
|
|
34
|
+
expect(taskV1.status).toBe(TaskStatus.Waiting);
|
|
35
|
+
expect(taskV1.unresolvedScheduleDependencies).toBe(2);
|
|
36
|
+
// 2. Replacement enqueue with only C
|
|
37
|
+
const taskV2 = await queue.enqueue('main', { version: 2 }, {
|
|
38
|
+
idempotencyKey,
|
|
39
|
+
scheduleAfter: [depC.id],
|
|
40
|
+
replace: true,
|
|
41
|
+
});
|
|
42
|
+
expect(taskV2.id).toBe(taskV1.id);
|
|
43
|
+
expect(taskV2.unresolvedScheduleDependencies).toBe(1);
|
|
44
|
+
expect(taskV2.data).toEqual({ version: 2 });
|
|
45
|
+
// 3. Complete an OLD dependency (depA)
|
|
46
|
+
const dDepA = await queue.dequeue({ types: ['depA'] });
|
|
47
|
+
await queue.complete(dDepA);
|
|
48
|
+
// Give it a moment to process background dependency resolution if any
|
|
49
|
+
await timeout(100);
|
|
50
|
+
// 4. Verify main task is STILL waiting for depC
|
|
51
|
+
const uTask = await queue.getTask(taskV1.id);
|
|
52
|
+
expect(uTask?.status).toBe(TaskStatus.Waiting);
|
|
53
|
+
expect(uTask?.unresolvedScheduleDependencies).toBe(1);
|
|
54
|
+
// 5. Complete NEW dependency (depC)
|
|
55
|
+
const dDepC = await queue.dequeue({ types: ['depC'] });
|
|
56
|
+
await queue.complete(dDepC);
|
|
57
|
+
// 6. Verify main task is now Pending
|
|
58
|
+
const fTask = await queue.getTask(taskV1.id);
|
|
59
|
+
expect(fTask?.status).toBe(TaskStatus.Pending);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { TaskQueueProvider } from '../../task-queue/index.js';
|
|
3
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
+
describe('Missing Idempotent Tasks Bug', () => {
|
|
5
|
+
let injector;
|
|
6
|
+
let queue;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
9
|
+
});
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
12
|
+
const queueName = `missing-idem-queue-${Date.now()}-${Math.random()}`;
|
|
13
|
+
queue = queueProvider.get(queueName, {
|
|
14
|
+
visibilityTimeout: 1000,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await queue.clear();
|
|
19
|
+
});
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await injector?.dispose();
|
|
22
|
+
});
|
|
23
|
+
it('should NOT crash when enqueuing duplicate idempotent tasks with dependencies and returnTasks: false', async () => {
|
|
24
|
+
const key = 'idem-key';
|
|
25
|
+
const dep = await queue.enqueue('dep', {});
|
|
26
|
+
// 1. Initial enqueue
|
|
27
|
+
await queue.enqueue('main', {}, { idempotencyKey: key });
|
|
28
|
+
// 2. Second enqueue with same key and a dependency, returnTasks: false
|
|
29
|
+
// This should NOT crash.
|
|
30
|
+
// It should ideally either:
|
|
31
|
+
// a) Ignore the new dependency because it's a duplicate and replace=false
|
|
32
|
+
// b) Add the dependency to the existing task (though replace=false usually means ignore)
|
|
33
|
+
// The current bug is that it CRASHES with TypeError.
|
|
34
|
+
await expect(queue.enqueueMany([
|
|
35
|
+
{ type: 'main', data: {}, idempotencyKey: key, scheduleAfter: [dep.id] }
|
|
36
|
+
], { replace: false, returnTasks: false })).resolves.toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -5,10 +5,10 @@ import { setupIntegrationTest } from '../../testing/index.js';
|
|
|
5
5
|
import { currentTimestamp } from '../../utils/date-time.js';
|
|
6
6
|
import { Timer } from '../../utils/timer.js';
|
|
7
7
|
import { timeout } from '../../utils/timing.js';
|
|
8
|
+
import { isDefined } from '../../utils/type-guards.js';
|
|
8
9
|
describe('Queue Integration Tests', () => {
|
|
9
10
|
let injector;
|
|
10
11
|
let queue;
|
|
11
|
-
const queueName = `test-queue-${Date.now()}`;
|
|
12
12
|
// Helper to verify state in DB
|
|
13
13
|
async function assertTaskStatus(id, state, message) {
|
|
14
14
|
const task = await queue.getTask(id);
|
|
@@ -16,10 +16,10 @@ describe('Queue Integration Tests', () => {
|
|
|
16
16
|
}
|
|
17
17
|
beforeAll(async () => {
|
|
18
18
|
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// messing with `injectArgument` manually in the test body.
|
|
19
|
+
});
|
|
20
|
+
beforeEach(async () => {
|
|
22
21
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
22
|
+
const queueName = `test-queue-${crypto.randomUUID()}`;
|
|
23
23
|
queue = queueProvider.get(queueName, {
|
|
24
24
|
visibilityTimeout: 5000,
|
|
25
25
|
retryDelayMinimum: 100, // Fast retries for testing
|
|
@@ -32,7 +32,9 @@ describe('Queue Integration Tests', () => {
|
|
|
32
32
|
});
|
|
33
33
|
afterAll(async () => {
|
|
34
34
|
try {
|
|
35
|
-
|
|
35
|
+
if (isDefined(queue)) {
|
|
36
|
+
await queue.clear();
|
|
37
|
+
}
|
|
36
38
|
await injector.dispose();
|
|
37
39
|
}
|
|
38
40
|
catch (error) {
|
|
@@ -76,9 +78,9 @@ describe('Queue Integration Tests', () => {
|
|
|
76
78
|
expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
|
|
77
79
|
const check1 = await queue.getTask(t1.id);
|
|
78
80
|
expect((check1?.data)['value']).toBe('original');
|
|
79
|
-
// 3. Replace Strategy: Should replace existing task with new data
|
|
81
|
+
// 3. Replace Strategy: Should replace existing task with new data. ID stays the same to avoid foreign key violations.
|
|
80
82
|
const t3 = await queue.enqueueMany([{ type: 'test', data: { value: 'updated' }, idempotencyKey: key }], { replace: true, returnTasks: true });
|
|
81
|
-
expect(t3[0].id, '
|
|
83
|
+
expect(t3[0].id, 'Same ID if replaced').toBe(t1.id);
|
|
82
84
|
// New task should have new data
|
|
83
85
|
const checkNew = await queue.getTask(t3[0].id);
|
|
84
86
|
expect((checkNew?.data)['value']).toBe('updated');
|
|
@@ -278,7 +280,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
278
280
|
const initialLock = dequeued.visibilityDeadline;
|
|
279
281
|
await timeout(20);
|
|
280
282
|
const touched = await queue.touch(dequeued);
|
|
281
|
-
expect(touched
|
|
283
|
+
expect(touched.visibilityDeadline > initialLock).toBe(true);
|
|
282
284
|
});
|
|
283
285
|
it('should prevent touching if token is lost (stolen by another worker)', async () => {
|
|
284
286
|
await queue.enqueue('foo', { foo: 'work' });
|
|
@@ -419,26 +421,33 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
419
421
|
expect(result.cancelled).toBe(false);
|
|
420
422
|
});
|
|
421
423
|
it('should wait for parent task to reach finalized state after child completion', async () => {
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
424
|
+
const parent = await queue.enqueue('parent', { value: 'parent' });
|
|
425
|
+
const dParent = await queue.dequeue({ types: ['parent'] });
|
|
426
|
+
expect(dParent).toBeDefined();
|
|
425
427
|
// Spawn a child
|
|
426
|
-
await queue.enqueueMany([{ type: '
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
428
|
+
const [child] = await queue.enqueueMany([{ type: 'child', data: { value: 'child' }, parentId: parent.id, completeAfter: [] }], { returnTasks: true });
|
|
429
|
+
expect(child).toBeDefined();
|
|
430
|
+
// Re-enqueuing with dependency:
|
|
431
|
+
const parentWithDep = await queue.enqueue('parent-dep', { value: 'parent' }, { completeAfter: [child.id] });
|
|
432
|
+
let dParent2;
|
|
433
|
+
for (let i = 0; i < 10; i++) {
|
|
434
|
+
dParent2 = await queue.dequeue({ types: ['parent-dep'] });
|
|
435
|
+
if (dParent2)
|
|
436
|
+
break;
|
|
437
|
+
await timeout(50);
|
|
438
|
+
}
|
|
439
|
+
expect(dParent2?.id).toBe(parentWithDep.id);
|
|
440
|
+
// Complete parent (it will move to Waiting because of completeAfter)
|
|
441
|
+
await queue.complete(dParent2);
|
|
442
|
+
const checkParent = await queue.getTask(parentWithDep.id);
|
|
443
|
+
expect(checkParent?.status).toBe(TaskStatus.WaitingChildren);
|
|
431
444
|
void (async () => {
|
|
432
445
|
await timeout(100);
|
|
433
|
-
const dChild = await queue.dequeue();
|
|
434
|
-
|
|
435
|
-
await queue.complete(dChild);
|
|
436
|
-
}
|
|
437
|
-
// Manual fan-in processing since we are in a test environment and might want immediate result
|
|
438
|
-
await queue.processPendingFanIn();
|
|
446
|
+
const dChild = await queue.dequeue({ types: ['child'] });
|
|
447
|
+
await queue.complete(dChild);
|
|
439
448
|
})();
|
|
440
|
-
await queue.waitForTasks([
|
|
441
|
-
const finalParent = await queue.getTask(
|
|
449
|
+
await queue.waitForTasks([parentWithDep.id], { timeout: 2000 });
|
|
450
|
+
const finalParent = await queue.getTask(parentWithDep.id);
|
|
442
451
|
expect(finalParent?.status).toBe(TaskStatus.Completed);
|
|
443
452
|
});
|
|
444
453
|
});
|