@tstdl/base 0.93.141 → 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.
Files changed (71) hide show
  1. package/authentication/client/authentication.service.d.ts +1 -0
  2. package/authentication/client/authentication.service.js +3 -2
  3. package/circuit-breaker/circuit-breaker.d.ts +6 -4
  4. package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
  5. package/circuit-breaker/postgres/circuit-breaker.js +8 -5
  6. package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
  7. package/examples/document-management/main.js +2 -2
  8. package/notification/tests/notification-api.test.js +5 -1
  9. package/notification/tests/notification-flow.test.js +9 -6
  10. package/orm/decorators.d.ts +17 -4
  11. package/orm/decorators.js +9 -0
  12. package/orm/server/bootstrap.d.ts +11 -0
  13. package/orm/server/bootstrap.js +31 -0
  14. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  15. package/orm/server/drizzle/schema-converter.js +71 -29
  16. package/orm/server/extension.d.ts +14 -0
  17. package/orm/server/extension.js +27 -0
  18. package/orm/server/index.d.ts +2 -0
  19. package/orm/server/index.js +2 -0
  20. package/orm/server/migration.d.ts +2 -3
  21. package/orm/server/migration.js +7 -21
  22. package/orm/server/repository.d.ts +1 -0
  23. package/orm/server/repository.js +19 -9
  24. package/orm/server/transaction.d.ts +1 -0
  25. package/orm/server/transaction.js +3 -0
  26. package/orm/tests/database-extension.test.js +63 -0
  27. package/orm/tests/database-migration.test.js +7 -6
  28. package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
  29. package/orm/tests/repository-compound-primary-key.test.js +234 -0
  30. package/orm/tests/schema-generation.test.d.ts +1 -0
  31. package/orm/tests/schema-generation.test.js +52 -5
  32. package/package.json +4 -4
  33. package/task-queue/README.md +0 -1
  34. package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
  35. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +151 -68
  36. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  37. package/task-queue/postgres/module.js +2 -1
  38. package/task-queue/postgres/schemas.d.ts +6 -0
  39. package/task-queue/postgres/task-queue.d.ts +18 -5
  40. package/task-queue/postgres/task-queue.js +593 -372
  41. package/task-queue/postgres/task.model.d.ts +9 -5
  42. package/task-queue/postgres/task.model.js +26 -26
  43. package/task-queue/task-context.d.ts +10 -5
  44. package/task-queue/task-context.js +5 -3
  45. package/task-queue/task-queue.d.ts +339 -35
  46. package/task-queue/task-queue.js +135 -31
  47. package/task-queue/tests/coverage-branch.test.js +45 -57
  48. package/task-queue/tests/coverage-enhancement.test.js +123 -117
  49. package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +61 -32
  50. package/task-queue/tests/dependencies.test.js +139 -21
  51. package/task-queue/tests/enqueue-batch.test.js +125 -0
  52. package/task-queue/tests/fan-out-spawning.test.js +43 -2
  53. package/task-queue/tests/idempotent-replacement.test.js +54 -1
  54. package/task-queue/tests/missing-idempotent-tasks.test.js +9 -8
  55. package/task-queue/tests/queue.test.js +261 -25
  56. package/task-queue/tests/shutdown.test.js +41 -0
  57. package/task-queue/tests/transactions.test.d.ts +1 -0
  58. package/task-queue/tests/transactions.test.js +47 -0
  59. package/task-queue/tests/worker.test.js +46 -13
  60. package/task-queue/tests/zombie-parent.test.js +1 -1
  61. package/task-queue/tests/zombie-recovery.test.js +3 -3
  62. package/testing/integration-setup.js +5 -3
  63. package/utils/timing.d.ts +2 -2
  64. package/task-queue/postgres/drizzle/0000_wakeful_sunspot.sql +0 -82
  65. package/task-queue/tests/cascading-cancellations.test.js +0 -38
  66. package/task-queue/tests/complex.test.js +0 -122
  67. package/task-queue/tests/dag-dependencies.test.js +0 -41
  68. /package/{task-queue/tests/cascading-cancellations.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
  69. /package/task-queue/tests/{complex.test.d.ts → dag.test.d.ts} +0 -0
  70. /package/task-queue/tests/{dag-dependencies.test.d.ts → enqueue-batch.test.d.ts} +0 -0
  71. /package/task-queue/tests/{extensive-dependencies.test.d.ts → shutdown.test.d.ts} +0 -0
@@ -21,124 +21,130 @@ describe('Task Queue Coverage Enhancement', () => {
21
21
  afterAll(async () => {
22
22
  await injector?.dispose();
23
23
  });
24
- it('should exercise TaskContext getters and methods', async () => {
25
- const queue = queueProvider.get(`context-test-${Date.now()}-${Math.random()}`);
26
- const task = await queue.enqueue('test', { foo: 'bar' }, { tags: ['tag1'], priority: 500 });
27
- const signal = new CancellationToken();
28
- const logger = injector.resolve(Logger);
29
- const context = new TaskContext(queue, task, signal, logger);
30
- expect(context.id).toBe(task.id);
31
- expect(context.type).toBe('test');
32
- expect(context.parentId).toBeNull();
33
- expect(context.tags).toContain('tag1');
34
- expect(context.unresolvedScheduleDependencies).toBe(0);
35
- expect(context.unresolvedCompleteDependencies).toBe(0);
36
- expect(context.data).toEqual({ foo: 'bar' });
37
- expect(context.state).toBeNull();
38
- expect(context.attempt).toBe(0);
39
- expect(context.triesLeft).toBe(queue.maxTries);
40
- expect(context.isFinalAttempt).toBe(false);
41
- expect(context.signal).toBe(signal);
42
- expect(context.logger).toBeDefined();
43
- // spawn for other queue
44
- const otherQueue = queueProvider.get(`other-queue-${Date.now()}-${Math.random()}`);
45
- const spawned = await context.spawn(otherQueue, 'spawned', { x: 1 });
46
- expect(spawned.type).toBe('spawned');
47
- const spawnedMany = await context.spawnMany(otherQueue, [{ type: 'm1', data: {} }, { type: 'm2', data: {} }]);
48
- expect(spawnedMany).toHaveLength(2);
49
- // checkpoint
50
- await context.checkpoint({ progress: 0.5 });
51
- // reschedule with delay
52
- await context.reschedule({ delay: 10000 });
53
- const uTask = await queue.getTask(task.id);
54
- expect(uTask?.status).toBe(TaskStatus.Pending);
55
- });
56
- it('should handle checkpoint lease loss', async () => {
57
- const queue = queueProvider.get(`checkpoint-loss-${Date.now()}-${Math.random()}`);
58
- const task = await queue.enqueue('t', {});
59
- const dTask = await queue.dequeue();
60
- // Manually break the token in DB
61
- const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
62
- await repository.useTransaction(undefined, async (tx) => {
63
- await tx.pgTransaction.update(taskTable).set({ token: RANDOM_UUID_V4 }).where(eq(taskTable.id, task.id));
24
+ describe('TaskContext', () => {
25
+ it('should correctly report basic properties', async () => {
26
+ const queue = queueProvider.get(`context-prop-${crypto.randomUUID()}`);
27
+ const task = await queue.enqueue('test', { foo: 'bar' }, { tags: ['tag1'], priority: 500 });
28
+ const context = new TaskContext(queue, task, new CancellationToken(), injector.resolve(Logger));
29
+ expect(context.id).toBe(task.id);
30
+ expect(context.type).toBe('test');
31
+ expect(context.parentId).toBeNull();
32
+ expect(context.tags).toContain('tag1');
33
+ expect(context.unresolvedScheduleDependencies).toBe(0);
34
+ expect(context.unresolvedCompleteDependencies).toBe(0);
35
+ expect(context.data).toEqual({ foo: 'bar' });
36
+ expect(context.state).toBeNull();
37
+ expect(context.attempt).toBe(0);
38
+ expect(context.triesLeft).toBe(queue.maxTries);
39
+ expect(context.isFinalAttempt).toBe(false);
40
+ });
41
+ it('should spawn children for other queues', async () => {
42
+ const queue = queueProvider.get(`context-spawn-${crypto.randomUUID()}`);
43
+ const otherQueue = queueProvider.get(`other-spawn-${crypto.randomUUID()}`);
44
+ const task = await queue.enqueue('parent', {});
45
+ const context = new TaskContext(queue, task, new CancellationToken(), injector.resolve(Logger));
46
+ const spawned = await context.spawn(otherQueue, 'spawned', { x: 1 });
47
+ expect(spawned.type).toBe('spawned');
48
+ expect(spawned.parentId).toBe(task.id);
49
+ expect(spawned.namespace).toBe(otherQueue.namespace);
50
+ const spawnedMany = await context.spawnMany(otherQueue, [{ type: 'm1', data: {} }, { type: 'm2', data: {} }]);
51
+ expect(spawnedMany).toHaveLength(2);
52
+ expect(spawnedMany.every(s => s.parentId == task.id)).toBe(true);
53
+ });
54
+ it('should handle checkpoint and reschedule', async () => {
55
+ const queue = queueProvider.get(`context-flow-${crypto.randomUUID()}`);
56
+ const task = await queue.enqueue('test', {});
57
+ const dTask = await queue.dequeue();
58
+ const context = new TaskContext(queue, dTask, new CancellationToken(), injector.resolve(Logger));
59
+ await context.checkpoint({ progress: 0.5, state: { step: 1 } });
60
+ const midTask = await queue.getTask(task.id);
61
+ expect(midTask?.progress).toBe(0.5);
62
+ expect(midTask?.state).toEqual({ step: 1 });
63
+ await context.reschedule({ delay: 10000 });
64
+ const finalTask = await queue.getTask(task.id);
65
+ expect(finalTask?.status).toBe(TaskStatus.Pending);
66
+ });
67
+ it('should handle checkpoint lease loss', async () => {
68
+ const queue = queueProvider.get(`checkpoint-loss-${crypto.randomUUID()}`);
69
+ const task = await queue.enqueue('t', {});
70
+ const dTask = await queue.dequeue();
71
+ // Manually break the token in DB
72
+ const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
73
+ await repository.useTransaction(undefined, async (tx) => {
74
+ await tx.pgTransaction.update(taskTable).set({ token: RANDOM_UUID_V4 }).where(eq(taskTable.id, task.id));
75
+ });
76
+ const signal = new CancellationToken();
77
+ const context = new TaskContext(queue, dTask, signal, injector.resolve(Logger));
78
+ await context.checkpoint({ progress: 0.9 });
79
+ expect(signal.isSet).toBe(true);
64
80
  });
65
- const signal = new CancellationToken();
66
- const logger = injector.resolve(Logger);
67
- const context = new TaskContext(queue, dTask, signal, logger);
68
- await context.checkpoint({ progress: 0.9 });
69
- expect(signal.isSet).toBe(true);
70
- });
71
- it('should exercise various TaskQueue methods', async () => {
72
- const queue = queueProvider.get(`methods-test-${Date.now()}-${Math.random()}`);
73
- // has
74
- expect(await queue.has('00000000-0000-0000-0000-000000000000')).toBe(false);
75
- const t = await queue.enqueue('t', {});
76
- expect(await queue.has(t.id)).toBe(true);
77
- // countByTags
78
- expect(await queue.countByTags([])).toBe(0);
79
- expect(await queue.countByTags(['tag1'])).toBe(0);
80
- await queue.enqueue('t2', {}, { tags: ['tag1'] });
81
- expect(await queue.countByTags(['tag1'])).toBe(1);
82
- // getManyByTags
83
- expect(await queue.getManyByTags([])).toEqual([]);
84
- const tagged = await queue.getManyByTags(['tag1']);
85
- expect(tagged).toHaveLength(1);
86
- // clear
87
- await queue.clear();
88
- expect(await queue.countByTags(['tag1'])).toBe(0);
89
- });
90
- it('should load task from archive in getTask', async () => {
91
- const archiveQueue = queueProvider.get(`archive-test-${Date.now()}-${Math.random()}`, { retention: 0, archiveRetention: 3600000 });
92
- const task = await archiveQueue.enqueue('t', {});
93
- const dTask = await archiveQueue.dequeue();
94
- await archiveQueue.complete(dTask);
95
- await timeout(10);
96
- await archiveQueue.maintenance();
97
- const archivedTask = await archiveQueue.getTask(task.id);
98
- expect(archivedTask).toBeDefined();
99
- expect(archivedTask?.id).toBe(task.id);
100
- expect(archivedTask?.status).toBe(TaskStatus.Completed);
101
- });
102
- it('should exercise touchMany and failMany', async () => {
103
- const isolatedQueue = queueProvider.get(`touch-fail-queue-${Date.now()}-${Math.random()}`);
104
- const t1 = await isolatedQueue.enqueue('t1', {});
105
- const t2 = await isolatedQueue.enqueue('t2', {});
106
- const dTasks = await isolatedQueue.dequeueMany(2);
107
- expect(dTasks).toHaveLength(2);
108
- const touched = await isolatedQueue.touchMany(dTasks, { progresses: [0.5, 0.8] });
109
- expect(touched).toHaveLength(2);
110
- expect(touched).toContain(t1.id);
111
- expect(touched).toContain(t2.id);
112
- await isolatedQueue.failMany(dTasks, [new Error('e1'), new Error('e2')]);
113
- const u1 = await isolatedQueue.getTask(t1.id);
114
- const u2 = await isolatedQueue.getTask(t2.id);
115
- expect(u1?.status).toBe(TaskStatus.Pending);
116
- expect(u2?.status).toBe(TaskStatus.Pending);
117
- expect(u1?.tries).toBe(1);
118
- });
119
- it('should handle idempotency collisions in enqueueMany', async () => {
120
- const queue = queueProvider.get(`idempotency-test-${Date.now()}-${Math.random()}`);
121
- const key = `idempotent-${Date.now()}-${Math.random()}`;
122
- await queue.enqueue('t1', { val: 1 }, { idempotencyKey: key });
123
- // Enqueue again with same key, returnTasks: true
124
- const tasks = await queue.enqueueMany([
125
- { type: 't1', data: { val: 2 }, idempotencyKey: key },
126
- { type: 't2', data: { val: 3 } }
127
- ], { returnTasks: true });
128
- expect(tasks).toHaveLength(2);
129
- const rt1 = tasks.find((t) => t.idempotencyKey == key);
130
- const rt2 = tasks.find((t) => t.idempotencyKey == null);
131
- expect(rt1?.data).toEqual({ val: 1 }); // Original data preserved
132
- expect(rt2?.data).toEqual({ val: 3 }); // New task created
133
81
  });
134
- it('should purge archive in maintenance', async () => {
135
- const purgeQueue = queueProvider.get(`purge-test-${Date.now()}-${Math.random()}`, { retention: 0, archiveRetention: 0 });
136
- const task = await purgeQueue.enqueue('t', {});
137
- const dTask = await purgeQueue.dequeue();
138
- await purgeQueue.complete(dTask);
139
- await timeout(10);
140
- await purgeQueue.maintenance(); // Should archive AND purge
141
- const purgedTask = await purgeQueue.getTask(task.id);
142
- expect(purgedTask).toBeUndefined();
82
+ describe('TaskQueue Utilities', () => {
83
+ it('should check for task existence', async () => {
84
+ const queue = queueProvider.get(`has-test-${crypto.randomUUID()}`);
85
+ expect(await queue.has(crypto.randomUUID())).toBe(false);
86
+ const t = await queue.enqueue('t', {});
87
+ expect(await queue.has(t.id)).toBe(true);
88
+ });
89
+ it('should count and retrieve tasks by tags', async () => {
90
+ const queue = queueProvider.get(`tags-test-${crypto.randomUUID()}`);
91
+ await queue.enqueue('t1', {}, { tags: ['tag1'] });
92
+ await queue.enqueue('t2', {}, { tags: ['tag1', 'tag2'] });
93
+ expect(await queue.countByTags(['tag1'])).toBe(2);
94
+ expect(await queue.countByTags(['tag2'])).toBe(1);
95
+ const tagged = await queue.getManyByTags(['tag2']);
96
+ expect(tagged).toHaveLength(1);
97
+ expect(tagged[0]?.type).toBe('t2');
98
+ });
99
+ it('should clear all tasks in namespace', async () => {
100
+ const queue = queueProvider.get(`clear-test-${crypto.randomUUID()}`);
101
+ await queue.enqueue('t', {});
102
+ await queue.clear();
103
+ expect(await queue.count()).toBe(0);
104
+ });
105
+ it('should load task from archive in getTask', async () => {
106
+ const archiveQueue = queueProvider.get(`archive-test-${crypto.randomUUID()}`, { retention: 0, archiveRetention: 3600000 });
107
+ const task = await archiveQueue.enqueue('t', {});
108
+ const dTask = await archiveQueue.dequeue();
109
+ await archiveQueue.complete(dTask);
110
+ await timeout(10);
111
+ await archiveQueue.maintenance();
112
+ const archivedTask = await archiveQueue.getTask(task.id);
113
+ expect(archivedTask).toBeDefined();
114
+ expect(archivedTask?.status).toBe(TaskStatus.Completed);
115
+ });
116
+ it('should handle batch touch and fail', async () => {
117
+ const isolatedQueue = queueProvider.get(`batch-util-${crypto.randomUUID()}`);
118
+ const t1 = await isolatedQueue.enqueue('t1', {});
119
+ const t2 = await isolatedQueue.enqueue('t2', {});
120
+ const dTasks = await isolatedQueue.dequeueMany(2);
121
+ const touched = await isolatedQueue.touchMany(dTasks, { progresses: [0.5, 0.8] });
122
+ expect(touched).toHaveLength(2);
123
+ await isolatedQueue.failMany(dTasks, [new Error('e1'), new Error('e2')]);
124
+ const u1 = await isolatedQueue.getTask(t1.id);
125
+ expect(u1?.status).toBe(TaskStatus.Retrying);
126
+ expect(u1?.tries).toBe(1);
127
+ });
128
+ it('should handle idempotency collisions in enqueueMany', async () => {
129
+ const queue = queueProvider.get(`idem-coll-${crypto.randomUUID()}`);
130
+ const key = `key-${crypto.randomUUID()}`;
131
+ await queue.enqueue('t1', { val: 1 }, { idempotencyKey: key });
132
+ const tasks = await queue.enqueueMany([
133
+ { type: 't1', data: { val: 2 }, idempotencyKey: key },
134
+ { type: 't2', data: { val: 3 } },
135
+ ], { returnTasks: true });
136
+ expect(tasks).toHaveLength(2);
137
+ expect(tasks.find(t => t.idempotencyKey == key)?.data).toEqual({ val: 1 });
138
+ });
139
+ it('should purge archive in maintenance', async () => {
140
+ const purgeQueue = queueProvider.get(`purge-test-${crypto.randomUUID()}`, { retention: 0, archiveRetention: 0 });
141
+ const task = await purgeQueue.enqueue('t', {});
142
+ const dTask = await purgeQueue.dequeue();
143
+ await purgeQueue.complete(dTask);
144
+ await timeout(10);
145
+ await purgeQueue.maintenance();
146
+ const purgedTask = await purgeQueue.getTask(task.id);
147
+ expect(purgedTask).toBeUndefined();
148
+ });
143
149
  });
144
150
  });
@@ -10,7 +10,7 @@ describe('Extensive Task Queue Dependency Tests', () => {
10
10
  });
11
11
  beforeEach(() => {
12
12
  const queueProvider = injector.resolve(TaskQueueProvider);
13
- const queueName = `extensive-dep-queue-${Date.now()}-${Math.random()}`;
13
+ const queueName = `extensive-dep-queue-${crypto.randomUUID()}`;
14
14
  queue = queueProvider.get(queueName, {
15
15
  visibilityTimeout: 1000,
16
16
  });
@@ -21,16 +21,6 @@ describe('Extensive Task Queue Dependency Tests', () => {
21
21
  afterAll(async () => {
22
22
  await injector?.dispose();
23
23
  });
24
- async function waitForStatus(id, status) {
25
- for (let i = 0; i < 100; i++) {
26
- const task = await queue.getTask(id);
27
- if (task?.status === status)
28
- return;
29
- await timeout(50);
30
- }
31
- const finalTask = await queue.getTask(id);
32
- throw new Error(`Task ${id} did not reach status ${status}. Current status: ${finalTask?.status}`);
33
- }
34
24
  async function completeTask(type) {
35
25
  const dequeued = await queue.dequeue({ types: [type] });
36
26
  if (!dequeued)
@@ -56,14 +46,13 @@ describe('Extensive Task Queue Dependency Tests', () => {
56
46
  expect(taskF.status).toBe(TaskStatus.Waiting);
57
47
  // 1. Complete A
58
48
  await completeTask('A');
59
- await waitForStatus(taskB.id, TaskStatus.Pending);
60
- await waitForStatus(taskC.id, TaskStatus.Pending);
49
+ await queue.waitForTasks([taskB.id, taskC.id], { statuses: [TaskStatus.Pending] });
61
50
  // 2. Complete B
62
51
  await completeTask('B');
63
- await waitForStatus(taskD.id, TaskStatus.Pending);
52
+ await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
64
53
  // 3. Complete C
65
54
  await completeTask('C');
66
- await waitForStatus(taskE.id, TaskStatus.Pending);
55
+ await queue.waitForTasks([taskE.id], { statuses: [TaskStatus.Pending] });
67
56
  // F should still be waiting
68
57
  const uF = await queue.getTask(taskF.id);
69
58
  expect(uF?.status).toBe(TaskStatus.Waiting);
@@ -73,7 +62,51 @@ describe('Extensive Task Queue Dependency Tests', () => {
73
62
  expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
74
63
  // 5. Complete E
75
64
  await completeTask('E');
76
- await waitForStatus(taskF.id, TaskStatus.Pending);
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);
77
110
  });
78
111
  it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
79
112
  const taskA = await queue.enqueue('A', {});
@@ -81,12 +114,11 @@ describe('Extensive Task Queue Dependency Tests', () => {
81
114
  const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
82
115
  const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
83
116
  await completeTask('A');
84
- await waitForStatus(taskB.id, TaskStatus.Pending);
85
- await waitForStatus(taskC.id, TaskStatus.Pending);
117
+ await queue.waitForTasks([taskB.id, taskC.id], { statuses: [TaskStatus.Pending] });
86
118
  await completeTask('B');
87
119
  expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
88
120
  await completeTask('C');
89
- await waitForStatus(taskD.id, TaskStatus.Pending);
121
+ await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
90
122
  });
91
123
  it('should handle Deep Chain (A -> B -> C -> D)', async () => {
92
124
  const taskA = await queue.enqueue('A', {});
@@ -94,11 +126,11 @@ describe('Extensive Task Queue Dependency Tests', () => {
94
126
  const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
95
127
  const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
96
128
  await completeTask('A');
97
- await waitForStatus(taskB.id, TaskStatus.Pending);
129
+ await queue.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending] });
98
130
  await completeTask('B');
99
- await waitForStatus(taskC.id, TaskStatus.Pending);
131
+ await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
100
132
  await completeTask('C');
101
- await waitForStatus(taskD.id, TaskStatus.Pending);
133
+ await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
102
134
  });
103
135
  it('should handle wide fan-out (A -> [B1...B50] -> C)', async () => {
104
136
  const taskA = await queue.enqueue('A', {});
@@ -110,7 +142,7 @@ describe('Extensive Task Queue Dependency Tests', () => {
110
142
  expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
111
143
  }
112
144
  await completeTask('B49');
113
- await waitForStatus(taskC.id, TaskStatus.Pending);
145
+ await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
114
146
  });
115
147
  it('should handle deep tree with branching and merging', async () => {
116
148
  const taskA = await queue.enqueue('A', {});
@@ -123,13 +155,13 @@ describe('Extensive Task Queue Dependency Tests', () => {
123
155
  await completeTask('A');
124
156
  await completeTask('B1');
125
157
  await completeTask('B2');
126
- await waitForStatus(taskC.id, TaskStatus.Pending);
158
+ await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
127
159
  await completeTask('C');
128
- await waitForStatus(taskD1.id, TaskStatus.Pending);
129
- await waitForStatus(taskD2.id, TaskStatus.Pending);
160
+ await queue.waitForTasks([taskD1.id], { statuses: [TaskStatus.Pending] });
161
+ await queue.waitForTasks([taskD2.id], { statuses: [TaskStatus.Pending] });
130
162
  await completeTask('D1');
131
163
  await completeTask('D2');
132
- await waitForStatus(taskE.id, TaskStatus.Pending);
164
+ await queue.waitForTasks([taskE.id], { statuses: [TaskStatus.Pending] });
133
165
  });
134
166
  it('should handle fail-fast cascade', async () => {
135
167
  const taskA = await queue.enqueue('A', {});
@@ -139,10 +171,7 @@ describe('Extensive Task Queue Dependency Tests', () => {
139
171
  const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id], failFast: true });
140
172
  const dA = await queue.dequeue({ types: ['A'] });
141
173
  await queue.fail(dA, new Error('fatal'), { fatal: true });
142
- await waitForStatus(taskB.id, TaskStatus.Dead);
143
- await waitForStatus(taskC.id, TaskStatus.Dead);
144
- await waitForStatus(taskD.id, TaskStatus.Dead);
145
- await waitForStatus(taskE.id, TaskStatus.Dead);
174
+ await queue.waitForTasks([taskB.id, taskC.id, taskD.id, taskE.id], { statuses: [TaskStatus.Skipped] });
146
175
  });
147
176
  it('should NOT schedule if only one of two dependencies is met (AND logic)', async () => {
148
177
  const taskA = await queue.enqueue('A', {});
@@ -154,6 +183,6 @@ describe('Extensive Task Queue Dependency Tests', () => {
154
183
  await timeout(100);
155
184
  expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
156
185
  await completeTask('B');
157
- await waitForStatus(taskC.id, TaskStatus.Pending);
186
+ await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
158
187
  });
159
188
  });