@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.
- package/authentication/client/authentication.service.d.ts +1 -0
- package/authentication/client/authentication.service.js +3 -2
- package/circuit-breaker/circuit-breaker.d.ts +6 -4
- package/circuit-breaker/postgres/circuit-breaker.d.ts +1 -0
- package/circuit-breaker/postgres/circuit-breaker.js +8 -5
- package/circuit-breaker/tests/circuit-breaker.test.js +20 -0
- package/examples/document-management/main.js +2 -2
- package/notification/tests/notification-api.test.js +5 -1
- package/notification/tests/notification-flow.test.js +9 -6
- package/orm/decorators.d.ts +17 -4
- package/orm/decorators.js +9 -0
- package/orm/server/bootstrap.d.ts +11 -0
- package/orm/server/bootstrap.js +31 -0
- package/orm/server/drizzle/schema-converter.d.ts +3 -1
- package/orm/server/drizzle/schema-converter.js +71 -29
- package/orm/server/extension.d.ts +14 -0
- package/orm/server/extension.js +27 -0
- package/orm/server/index.d.ts +2 -0
- package/orm/server/index.js +2 -0
- package/orm/server/migration.d.ts +2 -3
- package/orm/server/migration.js +7 -21
- package/orm/server/repository.d.ts +1 -0
- package/orm/server/repository.js +19 -9
- package/orm/server/transaction.d.ts +1 -0
- package/orm/server/transaction.js +3 -0
- package/orm/tests/database-extension.test.js +63 -0
- package/orm/tests/database-migration.test.js +7 -6
- package/orm/tests/repository-compound-primary-key.test.d.ts +2 -0
- package/orm/tests/repository-compound-primary-key.test.js +234 -0
- package/orm/tests/schema-generation.test.d.ts +1 -0
- package/orm/tests/schema-generation.test.js +52 -5
- package/package.json +4 -4
- package/task-queue/README.md +0 -1
- package/task-queue/postgres/drizzle/0000_great_gwen_stacy.sql +84 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +151 -68
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.js +2 -1
- package/task-queue/postgres/schemas.d.ts +6 -0
- package/task-queue/postgres/task-queue.d.ts +18 -5
- package/task-queue/postgres/task-queue.js +593 -372
- package/task-queue/postgres/task.model.d.ts +9 -5
- package/task-queue/postgres/task.model.js +26 -26
- package/task-queue/task-context.d.ts +10 -5
- package/task-queue/task-context.js +5 -3
- package/task-queue/task-queue.d.ts +339 -35
- package/task-queue/task-queue.js +135 -31
- package/task-queue/tests/coverage-branch.test.js +45 -57
- package/task-queue/tests/coverage-enhancement.test.js +123 -117
- package/task-queue/tests/{extensive-dependencies.test.js → dag.test.js} +61 -32
- package/task-queue/tests/dependencies.test.js +139 -21
- package/task-queue/tests/enqueue-batch.test.js +125 -0
- package/task-queue/tests/fan-out-spawning.test.js +43 -2
- package/task-queue/tests/idempotent-replacement.test.js +54 -1
- package/task-queue/tests/missing-idempotent-tasks.test.js +9 -8
- package/task-queue/tests/queue.test.js +261 -25
- package/task-queue/tests/shutdown.test.js +41 -0
- package/task-queue/tests/transactions.test.d.ts +1 -0
- package/task-queue/tests/transactions.test.js +47 -0
- package/task-queue/tests/worker.test.js +46 -13
- package/task-queue/tests/zombie-parent.test.js +1 -1
- package/task-queue/tests/zombie-recovery.test.js +3 -3
- package/testing/integration-setup.js +5 -3
- package/utils/timing.d.ts +2 -2
- package/task-queue/postgres/drizzle/0000_wakeful_sunspot.sql +0 -82
- package/task-queue/tests/cascading-cancellations.test.js +0 -38
- package/task-queue/tests/complex.test.js +0 -122
- package/task-queue/tests/dag-dependencies.test.js +0 -41
- /package/{task-queue/tests/cascading-cancellations.test.d.ts → orm/tests/database-extension.test.d.ts} +0 -0
- /package/task-queue/tests/{complex.test.d.ts → dag.test.d.ts} +0 -0
- /package/task-queue/tests/{dag-dependencies.test.d.ts → enqueue-batch.test.d.ts} +0 -0
- /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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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-${
|
|
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
|
|
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
|
|
52
|
+
await queue.waitForTasks([taskD.id], { statuses: [TaskStatus.Pending] });
|
|
64
53
|
// 3. Complete C
|
|
65
54
|
await completeTask('C');
|
|
66
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
129
|
+
await queue.waitForTasks([taskB.id], { statuses: [TaskStatus.Pending] });
|
|
98
130
|
await completeTask('B');
|
|
99
|
-
await
|
|
131
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
100
132
|
await completeTask('C');
|
|
101
|
-
await
|
|
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
|
|
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
|
|
158
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
127
159
|
await completeTask('C');
|
|
128
|
-
await
|
|
129
|
-
await
|
|
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
|
|
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
|
|
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
|
|
186
|
+
await queue.waitForTasks([taskC.id], { statuses: [TaskStatus.Pending] });
|
|
158
187
|
});
|
|
159
188
|
});
|