@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
|
@@ -10,7 +10,7 @@ describe('Zombie Recovery Race Condition', () => {
|
|
|
10
10
|
});
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
-
const queueName = `zombie-recovery-queue-${
|
|
13
|
+
const queueName = `zombie-recovery-queue-${crypto.randomUUID()}`;
|
|
14
14
|
queue = queueProvider.get(queueName, {
|
|
15
15
|
visibilityTimeout: 100, // Very short visibility timeout
|
|
16
16
|
retryDelayMinimum: 0,
|
|
@@ -37,7 +37,7 @@ describe('Zombie Recovery Race Condition', () => {
|
|
|
37
37
|
await queue.maintenance();
|
|
38
38
|
// Verify it was recovered to Pending
|
|
39
39
|
const recoveredParent = await queue.getTask(parent.id);
|
|
40
|
-
expect(recoveredParent?.status).toBe(TaskStatus.
|
|
40
|
+
expect(recoveredParent?.status).toBe(TaskStatus.Retrying);
|
|
41
41
|
// If the bug exists, startTimestamp is still set here.
|
|
42
42
|
// 6. Complete the child while parent is still Pending
|
|
43
43
|
const dChild = await queue.dequeue({ types: ['child'] });
|
|
@@ -46,6 +46,6 @@ describe('Zombie Recovery Race Condition', () => {
|
|
|
46
46
|
// BUG: evaluateTaskStatus will see unresolvedCompleteDependencies=0 and startTimestamp != null,
|
|
47
47
|
// and incorrectly transition the Pending parent to Completed.
|
|
48
48
|
const finalParent = await queue.getTask(parent.id);
|
|
49
|
-
expect(finalParent?.status).toBe(TaskStatus.
|
|
49
|
+
expect(finalParent?.status).toBe(TaskStatus.Retrying); // It should still be pending execution!
|
|
50
50
|
});
|
|
51
51
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { afterAll } from 'vitest';
|
|
2
3
|
import { configureApiServer } from '../api/server/index.js';
|
|
3
4
|
import { configureAudit } from '../audit/index.js';
|
|
4
5
|
import { AuthenticationApiClient } from '../authentication/client/api.client.js';
|
|
@@ -18,7 +19,7 @@ import { configureLocalMessageBus } from '../message-bus/index.js';
|
|
|
18
19
|
import { configureWebServerModule, WebServerModule } from '../module/modules/web-server.module.js';
|
|
19
20
|
import { configureNotification } from '../notification/server/index.js';
|
|
20
21
|
import { configureS3ObjectStorage } from '../object-storage/s3/index.js';
|
|
21
|
-
import { configureOrm, Database
|
|
22
|
+
import { bootstrapOrm, configureOrm, Database } from '../orm/server/index.js';
|
|
22
23
|
import { getEntitySchema, getEntityTableName } from '../orm/utils.js';
|
|
23
24
|
import { configurePostgresRateLimiter } from '../rate-limit/postgres/module.js';
|
|
24
25
|
import { configureDefaultSignalsImplementation } from '../signals/implementation/configure.js';
|
|
@@ -53,7 +54,7 @@ export async function setupIntegrationTest(options = {}) {
|
|
|
53
54
|
};
|
|
54
55
|
// 4. Configure ORM
|
|
55
56
|
// We disable autoMigrate here because APPLICATION_INITIALIZER is not used in integration tests
|
|
56
|
-
// We manually run migrations via
|
|
57
|
+
// We manually run migrations via bootstrapOrm below
|
|
57
58
|
configureOrm({
|
|
58
59
|
repositoryConfig: { schema: options.orm?.schema ?? 'test' },
|
|
59
60
|
connection: dbConfig,
|
|
@@ -118,7 +119,7 @@ export async function setupIntegrationTest(options = {}) {
|
|
|
118
119
|
injector,
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
|
-
await runInInjectionContext(injector,
|
|
122
|
+
await runInInjectionContext(injector, bootstrapOrm);
|
|
122
123
|
if (options.modules?.objectStorage) {
|
|
123
124
|
const bucketPerModule = options.s3?.bucketPerModule ?? configParser.boolean('S3_BUCKET_PER_MODULE', true);
|
|
124
125
|
configureS3ObjectStorage({
|
|
@@ -162,6 +163,7 @@ export async function setupIntegrationTest(options = {}) {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
});
|
|
166
|
+
afterAll(async () => await injector.dispose());
|
|
165
167
|
return { injector, database };
|
|
166
168
|
}
|
|
167
169
|
/**
|
package/utils/timing.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export declare function timeout(milliseconds?: number, options?: {
|
|
|
8
8
|
/** Timeout until specified time */
|
|
9
9
|
export declare function timeoutUntil(timestamp: number | Date): Promise<void>;
|
|
10
10
|
/** Timeout for specified duration */
|
|
11
|
-
export declare function cancelableTimeout(milliseconds: number, cancelSignal: Observable<
|
|
11
|
+
export declare function cancelableTimeout(milliseconds: number, cancelSignal: Observable<unknown> | CancellationSignal): Promise<'timeout' | 'canceled'>;
|
|
12
12
|
/** Timeout until specified time */
|
|
13
|
-
export declare function cancelableTimeoutUntil(timestamp: number | Date, cancelSignal: Observable<
|
|
13
|
+
export declare function cancelableTimeoutUntil(timestamp: number | Date, cancelSignal: Observable<unknown> | CancellationSignal): Promise<'timeout' | 'canceled'>;
|
|
14
14
|
export declare function withTimeout<T>(milliseconds: number, promiseOrProvider: ValueOrProvider<Promise<T>>, options?: {
|
|
15
15
|
errorMessage?: string;
|
|
16
16
|
}): Promise<T>;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
CREATE TYPE "task_queue"."task_dependency_type" AS ENUM('schedule', 'complete');--> statement-breakpoint
|
|
2
|
-
CREATE TYPE "task_queue"."task_status" AS ENUM('pending', 'running', 'completed', 'cancelled', 'dead', 'waiting', 'waiting-children', 'paused');--> statement-breakpoint
|
|
3
|
-
CREATE TABLE "task_queue"."task" (
|
|
4
|
-
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
5
|
-
"namespace" text NOT NULL,
|
|
6
|
-
"type" text NOT NULL,
|
|
7
|
-
"status" "task_queue"."task_status" NOT NULL,
|
|
8
|
-
"idempotency_key" text,
|
|
9
|
-
"trace_id" text,
|
|
10
|
-
"tags" text[] NOT NULL,
|
|
11
|
-
"fail_fast" boolean NOT NULL,
|
|
12
|
-
"priority" integer NOT NULL,
|
|
13
|
-
"unresolved_schedule_dependencies" integer NOT NULL,
|
|
14
|
-
"unresolved_complete_dependencies" integer NOT NULL,
|
|
15
|
-
"token" uuid,
|
|
16
|
-
"creation_timestamp" timestamp with time zone NOT NULL,
|
|
17
|
-
"priority_age_timestamp" timestamp with time zone NOT NULL,
|
|
18
|
-
"schedule_timestamp" timestamp with time zone NOT NULL,
|
|
19
|
-
"start_timestamp" timestamp with time zone,
|
|
20
|
-
"time_to_live" timestamp with time zone,
|
|
21
|
-
"visibility_deadline" timestamp with time zone,
|
|
22
|
-
"complete_timestamp" timestamp with time zone,
|
|
23
|
-
"tries" integer NOT NULL,
|
|
24
|
-
"progress" double precision NOT NULL,
|
|
25
|
-
"data" jsonb,
|
|
26
|
-
"state" jsonb,
|
|
27
|
-
"result" jsonb,
|
|
28
|
-
"error" jsonb,
|
|
29
|
-
"parent_id" uuid,
|
|
30
|
-
CONSTRAINT "task_namespace_id_unique" UNIQUE("namespace","id"),
|
|
31
|
-
CONSTRAINT "task_namespace_idempotency_key_unique" UNIQUE("namespace","idempotency_key")
|
|
32
|
-
);
|
|
33
|
-
--> statement-breakpoint
|
|
34
|
-
CREATE TABLE "task_queue"."task_archive" (
|
|
35
|
-
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
36
|
-
"namespace" text NOT NULL,
|
|
37
|
-
"type" text NOT NULL,
|
|
38
|
-
"status" "task_queue"."task_status" NOT NULL,
|
|
39
|
-
"idempotency_key" text,
|
|
40
|
-
"trace_id" text,
|
|
41
|
-
"tags" text[] NOT NULL,
|
|
42
|
-
"fail_fast" boolean NOT NULL,
|
|
43
|
-
"priority" integer NOT NULL,
|
|
44
|
-
"unresolved_schedule_dependencies" integer NOT NULL,
|
|
45
|
-
"unresolved_complete_dependencies" integer NOT NULL,
|
|
46
|
-
"token" uuid,
|
|
47
|
-
"creation_timestamp" timestamp with time zone NOT NULL,
|
|
48
|
-
"priority_age_timestamp" timestamp with time zone NOT NULL,
|
|
49
|
-
"schedule_timestamp" timestamp with time zone NOT NULL,
|
|
50
|
-
"start_timestamp" timestamp with time zone,
|
|
51
|
-
"time_to_live" timestamp with time zone,
|
|
52
|
-
"visibility_deadline" timestamp with time zone,
|
|
53
|
-
"complete_timestamp" timestamp with time zone,
|
|
54
|
-
"tries" integer NOT NULL,
|
|
55
|
-
"progress" double precision NOT NULL,
|
|
56
|
-
"data" jsonb,
|
|
57
|
-
"state" jsonb,
|
|
58
|
-
"result" jsonb,
|
|
59
|
-
"error" jsonb,
|
|
60
|
-
"parent_id" uuid
|
|
61
|
-
);
|
|
62
|
-
--> statement-breakpoint
|
|
63
|
-
CREATE TABLE "task_queue"."task_dependency" (
|
|
64
|
-
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
65
|
-
"namespace" text NOT NULL,
|
|
66
|
-
"task_id" uuid NOT NULL,
|
|
67
|
-
"dependency_task_id" uuid NOT NULL,
|
|
68
|
-
"type" "task_queue"."task_dependency_type" NOT NULL,
|
|
69
|
-
"required_statuses" "task_queue"."task_status"[] NOT NULL,
|
|
70
|
-
CONSTRAINT "td_namespace_task_id_dependency_task_id_type_unique" UNIQUE("namespace","task_id","dependency_task_id","type")
|
|
71
|
-
);
|
|
72
|
-
--> statement-breakpoint
|
|
73
|
-
ALTER TABLE "task_queue"."task" ADD CONSTRAINT "task_namespace_parentId_task_fkey" FOREIGN KEY ("namespace","parent_id") REFERENCES "task_queue"."task"("namespace","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
74
|
-
ALTER TABLE "task_queue"."task_dependency" ADD CONSTRAINT "task_dependency_namespace_dependencyTaskId_task_fkey" FOREIGN KEY ("namespace","dependency_task_id") REFERENCES "task_queue"."task"("namespace","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
75
|
-
ALTER TABLE "task_queue"."task_dependency" ADD CONSTRAINT "task_dependency_namespace_taskId_task_fkey" FOREIGN KEY ("namespace","task_id") REFERENCES "task_queue"."task"("namespace","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
76
|
-
CREATE INDEX "task_parent_id_idx" ON "task_queue"."task" USING btree ("parent_id");--> statement-breakpoint
|
|
77
|
-
CREATE INDEX "task_status_visibility_deadline_idx" ON "task_queue"."task" USING btree ("status","visibility_deadline");--> statement-breakpoint
|
|
78
|
-
CREATE INDEX "task_status_complete_timestamp_idx" ON "task_queue"."task" USING btree ("status","complete_timestamp");--> statement-breakpoint
|
|
79
|
-
CREATE INDEX "task_tags_idx" ON "task_queue"."task" USING gin ("tags");--> statement-breakpoint
|
|
80
|
-
CREATE INDEX "task_namespace_status_schedule_timestamp_priority_idx" ON "task_queue"."task" USING btree ("namespace","status","schedule_timestamp","priority");--> statement-breakpoint
|
|
81
|
-
CREATE INDEX "task_archive_namespace_complete_timestamp_idx" ON "task_queue"."task_archive" USING btree ("namespace","complete_timestamp");--> statement-breakpoint
|
|
82
|
-
CREATE INDEX "task_dependency_namespace_dependency_task_id_type_idx" ON "task_queue"."task_dependency" USING btree ("namespace","dependency_task_id","type");
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
describe('Cascading Cancellations', () => {
|
|
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 = `cancellation-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 recursively cancel children in ownership tree', async () => {
|
|
24
|
-
const root = await queue.enqueue('root', {});
|
|
25
|
-
const child1 = await queue.enqueue('child1', {}, { parentId: root.id });
|
|
26
|
-
const child2 = await queue.enqueue('child2', {}, { parentId: root.id });
|
|
27
|
-
const grandchild = await queue.enqueue('grandchild', {}, { parentId: child1.id });
|
|
28
|
-
await queue.cancel(root.id);
|
|
29
|
-
const uRoot = await queue.getTask(root.id);
|
|
30
|
-
const uChild1 = await queue.getTask(child1.id);
|
|
31
|
-
const uChild2 = await queue.getTask(child2.id);
|
|
32
|
-
const uGrandchild = await queue.getTask(grandchild.id);
|
|
33
|
-
expect(uRoot?.status).toBe(TaskStatus.Cancelled);
|
|
34
|
-
expect(uChild1?.status).toBe(TaskStatus.Cancelled);
|
|
35
|
-
expect(uChild2?.status).toBe(TaskStatus.Cancelled);
|
|
36
|
-
expect(uGrandchild?.status).toBe(TaskStatus.Cancelled);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
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('Complex Queue Scenarios', () => {
|
|
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 = `complex-queue-${Date.now()}-${Math.random()}`;
|
|
14
|
-
// Configure with specific settings for testing logic
|
|
15
|
-
queue = queueProvider.get(queueName, {
|
|
16
|
-
visibilityTimeout: 1000,
|
|
17
|
-
priorityAgingInterval: 50, // Fast aging
|
|
18
|
-
priorityAgingStep: 10,
|
|
19
|
-
rateLimit: 5,
|
|
20
|
-
rateInterval: 200,
|
|
21
|
-
retryDelayMinimum: 50,
|
|
22
|
-
retryDelayGrowth: 2,
|
|
23
|
-
retention: 50, // Fast retention for archive test
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
await queue.clear();
|
|
28
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
29
|
-
await queueProvider.get('other-queue').clear();
|
|
30
|
-
});
|
|
31
|
-
afterAll(async () => {
|
|
32
|
-
await injector?.dispose();
|
|
33
|
-
});
|
|
34
|
-
async function waitForStatus(id, status) {
|
|
35
|
-
for (let i = 0; i < 50; i++) {
|
|
36
|
-
const task = await queue.getTask(id);
|
|
37
|
-
if (task?.status === status)
|
|
38
|
-
return;
|
|
39
|
-
await timeout(10);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
describe('Complex Dependencies', () => {
|
|
43
|
-
it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
|
|
44
|
-
const taskA = await queue.enqueue('A', {});
|
|
45
|
-
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
46
|
-
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
|
|
47
|
-
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
|
|
48
|
-
expect(taskD.status).toBe(TaskStatus.Waiting);
|
|
49
|
-
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
50
|
-
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
51
|
-
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
52
|
-
// Process A
|
|
53
|
-
const dA = await queue.dequeue({ types: ['A'] });
|
|
54
|
-
await queue.complete(dA);
|
|
55
|
-
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
56
|
-
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
57
|
-
// Process B
|
|
58
|
-
const dB = await queue.dequeue({ types: ['B'] });
|
|
59
|
-
await queue.complete(dB);
|
|
60
|
-
// D still waiting (needs C)
|
|
61
|
-
const uD2 = await queue.getTask(taskD.id);
|
|
62
|
-
expect(uD2?.status).toBe(TaskStatus.Waiting);
|
|
63
|
-
// Process C
|
|
64
|
-
const dC = await queue.dequeue({ types: ['C'] });
|
|
65
|
-
await queue.complete(dC);
|
|
66
|
-
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
67
|
-
// D should be Pending
|
|
68
|
-
const uD3 = await queue.getTask(taskD.id);
|
|
69
|
-
expect(uD3?.status).toBe(TaskStatus.Pending);
|
|
70
|
-
});
|
|
71
|
-
it('should handle Deep Chain (A -> B -> C -> D)', async () => {
|
|
72
|
-
const taskA = await queue.enqueue('A', {});
|
|
73
|
-
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
|
|
74
|
-
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
|
|
75
|
-
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
|
|
76
|
-
await queue.complete((await queue.dequeue({ types: ['A'] })));
|
|
77
|
-
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
78
|
-
await queue.complete((await queue.dequeue({ types: ['B'] })));
|
|
79
|
-
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
80
|
-
await queue.complete((await queue.dequeue({ types: ['C'] })));
|
|
81
|
-
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
82
|
-
});
|
|
83
|
-
it('should respect failFast = false (continue other branches)', async () => {
|
|
84
|
-
const taskA = await queue.enqueue('A', {});
|
|
85
|
-
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: false });
|
|
86
|
-
const taskC = await queue.enqueue('C', {}); // Independent
|
|
87
|
-
const dA = await queue.dequeue({ types: ['A'] });
|
|
88
|
-
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
89
|
-
// taskB should stay Waiting (default requiredStatus is Completed)
|
|
90
|
-
// If failFast is false, it should transition to Pending once the dependency is terminal, even if it failed.
|
|
91
|
-
await timeout(100);
|
|
92
|
-
const uB = await queue.getTask(taskB.id);
|
|
93
|
-
expect(uB?.status).toBe(TaskStatus.Pending);
|
|
94
|
-
const dC = await queue.dequeue({ types: ['C'] });
|
|
95
|
-
expect(dC?.id).toBe(taskC.id);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe('Other Scenarios', () => {
|
|
99
|
-
it('should handle priority aging', async () => {
|
|
100
|
-
const t1 = await queue.enqueue('low', {}, { priority: 2000 });
|
|
101
|
-
const t2 = await queue.enqueue('high', {}, { priority: 1000 });
|
|
102
|
-
// Wait for aging to trigger
|
|
103
|
-
await timeout(100);
|
|
104
|
-
await queue.maintenance();
|
|
105
|
-
const u1 = await queue.getTask(t1.id);
|
|
106
|
-
const u2 = await queue.getTask(t2.id);
|
|
107
|
-
expect(u1.priority).toBeLessThan(2000);
|
|
108
|
-
expect(u2.priority).toBeLessThan(1000);
|
|
109
|
-
});
|
|
110
|
-
it('should handle rate limiting', async () => {
|
|
111
|
-
// Limit is 5 per 200ms
|
|
112
|
-
for (let i = 0; i < 10; i++) {
|
|
113
|
-
await queue.enqueue('task', { i });
|
|
114
|
-
}
|
|
115
|
-
const batch1 = await queue.dequeueMany(10);
|
|
116
|
-
expect(batch1.length).toBe(5);
|
|
117
|
-
await timeout(500); // Increased from 250
|
|
118
|
-
const batch2 = await queue.dequeueMany(10);
|
|
119
|
-
expect(batch2.length).toBe(5);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
describe('DAG Dependencies', () => {
|
|
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 = `dag-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 schedule a task only after dependency reaches required status (scheduleAfter)', async () => {
|
|
24
|
-
const prereq = await queue.enqueue('prereq', { val: 1 });
|
|
25
|
-
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
26
|
-
scheduleAfter: [prereq.id],
|
|
27
|
-
});
|
|
28
|
-
expect(dependent.status).toBe(TaskStatus.Waiting);
|
|
29
|
-
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
30
|
-
expect(dequeued?.id).toBe(prereq.id);
|
|
31
|
-
await queue.complete(dequeued);
|
|
32
|
-
// Dependent should transition to Pending.
|
|
33
|
-
// Let's dequeue and complete it to make it reaches a finalized state for waitForTasks
|
|
34
|
-
const dDependent = await queue.dequeue({ types: ['dependent'] });
|
|
35
|
-
expect(dDependent?.id).toBe(dependent.id);
|
|
36
|
-
await queue.complete(dDependent);
|
|
37
|
-
await queue.waitForTasks([dependent.id]);
|
|
38
|
-
const updatedDependent = await queue.getTask(dependent.id);
|
|
39
|
-
expect(updatedDependent?.status).toBe(TaskStatus.Completed);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|