@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.
Files changed (98) hide show
  1. package/application/application.d.ts +1 -1
  2. package/application/application.js +1 -1
  3. package/application/providers.d.ts +20 -2
  4. package/application/providers.js +34 -7
  5. package/audit/module.d.ts +5 -0
  6. package/audit/module.js +9 -1
  7. package/authentication/server/module.d.ts +5 -0
  8. package/authentication/server/module.js +9 -1
  9. package/authentication/tests/authentication.api-controller.test.js +1 -1
  10. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  11. package/authentication/tests/authentication.client-service.test.js +1 -1
  12. package/circuit-breaker/postgres/module.d.ts +1 -0
  13. package/circuit-breaker/postgres/module.js +5 -1
  14. package/document-management/server/configure.js +5 -1
  15. package/document-management/server/module.d.ts +1 -1
  16. package/document-management/server/module.js +1 -1
  17. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  18. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  19. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  20. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  21. package/examples/document-management/main.d.ts +1 -0
  22. package/examples/document-management/main.js +14 -11
  23. package/key-value-store/postgres/module.d.ts +1 -0
  24. package/key-value-store/postgres/module.js +5 -1
  25. package/lock/postgres/module.d.ts +1 -0
  26. package/lock/postgres/module.js +5 -1
  27. package/mail/module.d.ts +5 -1
  28. package/mail/module.js +11 -6
  29. package/module/modules/web-server.module.js +2 -3
  30. package/notification/server/module.d.ts +1 -0
  31. package/notification/server/module.js +5 -1
  32. package/notification/tests/notification-flow.test.js +2 -2
  33. package/orm/decorators.d.ts +5 -1
  34. package/orm/decorators.js +1 -1
  35. package/orm/server/drizzle/schema-converter.js +17 -30
  36. package/orm/server/encryption.d.ts +0 -1
  37. package/orm/server/encryption.js +1 -4
  38. package/orm/server/index.d.ts +1 -6
  39. package/orm/server/index.js +1 -6
  40. package/orm/server/migration.d.ts +19 -0
  41. package/orm/server/migration.js +72 -0
  42. package/orm/server/repository.d.ts +1 -1
  43. package/orm/server/transaction.d.ts +5 -10
  44. package/orm/server/transaction.js +22 -26
  45. package/orm/server/transactional.js +3 -3
  46. package/orm/tests/database-migration.test.d.ts +1 -0
  47. package/orm/tests/database-migration.test.js +82 -0
  48. package/orm/tests/encryption.test.js +3 -4
  49. package/orm/utils.d.ts +17 -2
  50. package/orm/utils.js +49 -1
  51. package/package.json +4 -3
  52. package/rate-limit/postgres/module.d.ts +1 -0
  53. package/rate-limit/postgres/module.js +5 -1
  54. package/reflection/decorator-data.js +11 -12
  55. package/task-queue/README.md +2 -9
  56. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  57. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  58. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  59. package/task-queue/postgres/module.d.ts +1 -0
  60. package/task-queue/postgres/module.js +5 -1
  61. package/task-queue/postgres/schemas.d.ts +9 -6
  62. package/task-queue/postgres/schemas.js +4 -3
  63. package/task-queue/postgres/task-queue.d.ts +2 -12
  64. package/task-queue/postgres/task-queue.js +431 -354
  65. package/task-queue/postgres/task.model.d.ts +12 -5
  66. package/task-queue/postgres/task.model.js +51 -25
  67. package/task-queue/task-context.d.ts +2 -2
  68. package/task-queue/task-context.js +7 -7
  69. package/task-queue/task-queue.d.ts +36 -19
  70. package/task-queue/task-queue.js +18 -10
  71. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  72. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  73. package/task-queue/tests/complex.test.js +44 -228
  74. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  75. package/task-queue/tests/coverage-branch.test.js +407 -0
  76. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  77. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  78. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  79. package/task-queue/tests/dag-dependencies.test.js +41 -0
  80. package/task-queue/tests/dependencies.test.js +26 -26
  81. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  82. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  83. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  84. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  85. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  86. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  87. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  88. package/task-queue/tests/queue.test.js +33 -24
  89. package/task-queue/tests/worker.test.js +20 -5
  90. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  91. package/task-queue/tests/zombie-parent.test.js +45 -0
  92. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  93. package/task-queue/tests/zombie-recovery.test.js +51 -0
  94. package/test5.js +5 -5
  95. package/testing/integration-setup.d.ts +4 -4
  96. package/testing/integration-setup.js +54 -29
  97. package/text/localization.service.js +2 -2
  98. 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 { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
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 (Fan-In)', () => {
36
- it('should schedule a task only after dependency completes (completeAfterTags)', async () => {
37
- // 1. Create a dependent task (Waiting)
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
- completeAfterTags: ['tag-a'],
40
+ completeAfter: [prereq.id],
40
41
  });
41
- expect(dependent.status).toBe(TaskStatus.Waiting);
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
- await queue.processPendingFanIn();
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 (scheduleAfterTags)', async () => {
54
- // 1. Dependent task
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
- scheduleAfterTags: ['tag-b'],
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
- scheduleAfterTags: ['tag-fail'],
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 DependencyJoinMode.Or', async () => {
88
- // Wait for tag-1 OR tag-2
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
- scheduleAfterTags: ['tag-1', 'tag-2'],
91
- dependencyJoinMode: DependencyJoinMode.Or
92
+ scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }]
92
93
  });
93
- // Complete tag-1 only
94
- await queue.enqueue('t1', {}, { tags: ['tag-1'] });
95
- const d1 = await queue.dequeue({ types: ['t1'] });
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 { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
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', {}, { tags: ['mc-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 requested pattern: A -> B & C -> D', async () => {
81
- const taskD = await queue.enqueue('D', {}, { scheduleAfterTags: ['B', 'C'] });
82
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
83
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A'] });
84
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
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
- // B and C can be dequeued now (in any order)
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
- // Complete C
106
- await queue.complete(dC);
88
+ await completeTask('C');
107
89
  await waitForStatus(taskD.id, TaskStatus.Pending);
108
90
  });
109
- it('should strictly adhere to order: A -> B -> C', async () => {
110
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'] });
111
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
112
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
113
- // Try to dequeue B and C - should fail
114
- const dC = await queue.dequeue({ types: ['C'] });
115
- const dB = await queue.dequeue({ types: ['B'] });
116
- expect(dC).toBeUndefined();
117
- expect(dB).toBeUndefined();
118
- // Complete A
119
- const dA = await queue.dequeue({ types: ['A'] });
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 large fan-in with OR: (T1, T2, T3, T4, T5) -> Result', async () => {
151
- const tags = ['o1', 'o2', 'o3', 'o4', 'o5'];
152
- const taskResult = await queue.enqueue('Result', {}, {
153
- scheduleAfterTags: tags,
154
- dependencyJoinMode: DependencyJoinMode.Or
155
- });
156
- for (const tag of tags) {
157
- await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
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
- // Complete one
160
- await completeTask(`Task-${tags[2]}`);
161
- await waitForStatus(taskResult.id, TaskStatus.Pending);
112
+ await completeTask('B49');
113
+ await waitForStatus(taskC.id, TaskStatus.Pending);
162
114
  });
163
- it('should handle Diamond of Diamonds: A -> (B1, B2) -> C -> (D1, D2) -> E', async () => {
164
- const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['tag-d1', 'tag-d2'] });
165
- const taskD1 = await queue.enqueue('D1', {}, { tags: ['tag-d1'], scheduleAfterTags: ['tag-c'] });
166
- const taskD2 = await queue.enqueue('D2', {}, { tags: ['tag-d2'], scheduleAfterTags: ['tag-c'] });
167
- const taskC = await queue.enqueue('C', {}, { tags: ['tag-c'], scheduleAfterTags: ['tag-b1', 'tag-b2'] });
168
- const taskB1 = await queue.enqueue('B1', {}, { tags: ['tag-b1'], scheduleAfterTags: ['tag-a'] });
169
- const taskB2 = await queue.enqueue('B2', {}, { tags: ['tag-b2'], scheduleAfterTags: ['tag-a'] });
170
- const taskA = await queue.enqueue('A', {}, { tags: ['tag-a'] });
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 entire branch if one dependency fails fatal', async () => {
188
- // A -> B -> C
189
- // -> D -> E
190
- // (C & E) -> F
191
- const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'E'], failFast: true });
192
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'], failFast: true });
193
- const taskE = await queue.enqueue('E', {}, { tags: ['E'], scheduleAfterTags: ['D'], failFast: true });
194
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'], failFast: true });
195
- const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A'], failFast: true });
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(taskF.id, TaskStatus.Dead);
207
- // D and E should be unaffected (except E is still waiting for D)
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 handle many-to-many dependencies', async () => {
212
- // {A, B} -> {C, D} -> {E, F}
213
- // Each of C, D depends on BOTH A and B.
214
- // Each of E, F depends on BOTH C and D.
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
- expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
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,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
- // Use the QueueProvider to Create/Retrieve the queue.
20
- // This is the cleanest way to get a configured queue instance without
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
- await queue.clear();
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 AND new ID
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, 'New ID if replaced').not.toBe(t1.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?.visibilityDeadline > initialLock).toBe(true);
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 childTag = `child-of-${crypto.randomUUID()}`;
423
- const parent = await queue.enqueue('test', { value: 'parent' }, { completeAfterTags: [childTag] });
424
- const dParent = await queue.dequeue();
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: 'test', data: { value: 'child' }, parentId: parent.id, tags: [childTag] }], { transaction: undefined });
427
- // Complete parent (it will move to Waiting because of completeAfterTags)
428
- await queue.complete(dParent);
429
- const checkParent = await queue.getTask(parent.id);
430
- expect(checkParent?.status).toBe(TaskStatus.Waiting);
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
- if (dChild) {
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([parent.id], { timeout: 2000 });
441
- const finalParent = await queue.getTask(parent.id);
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
  });