@tstdl/base 0.93.87 → 0.93.90
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/ai/genkit/helpers.d.ts +3 -1
- package/ai/genkit/helpers.js +3 -3
- package/api/server/gateway.d.ts +3 -0
- package/api/server/gateway.js +15 -4
- package/api/server/middlewares/catch-error.middleware.js +2 -4
- package/api/server/middlewares/cors.middleware.js +2 -3
- package/api/server/middlewares/csrf.middleware.d.ts +41 -0
- package/api/server/middlewares/csrf.middleware.js +108 -0
- package/api/server/middlewares/index.d.ts +1 -0
- package/api/server/middlewares/index.js +1 -0
- package/api/server/module.d.ts +8 -2
- package/api/server/module.js +14 -8
- package/api/server/tests/csrf.middleware.test.js +91 -0
- package/audit/drizzle/{0000_bored_stick.sql → 0000_lumpy_thunderball.sql} +3 -3
- package/audit/drizzle/meta/0000_snapshot.json +4 -4
- package/audit/drizzle/meta/_journal.json +2 -9
- package/audit/module.d.ts +4 -1
- package/audit/module.js +3 -2
- package/audit/schemas.d.ts +1 -1
- package/audit/types.d.ts +1 -1
- package/audit/types.js +1 -1
- package/authentication/client/authentication.service.d.ts +14 -1
- package/authentication/client/authentication.service.js +82 -23
- package/authentication/client/http-client.middleware.d.ts +6 -0
- package/authentication/client/http-client.middleware.js +36 -0
- package/authentication/client/module.js +8 -2
- package/authentication/models/service-account.model.d.ts +2 -2
- package/authentication/models/service-account.model.js +10 -5
- package/authentication/models/subject.model.d.ts +20 -5
- package/authentication/models/subject.model.js +34 -29
- package/authentication/models/system-account.model.d.ts +3 -2
- package/authentication/models/system-account.model.js +11 -5
- package/authentication/models/user.model.d.ts +2 -11
- package/authentication/models/user.model.js +5 -16
- package/authentication/server/authentication-api-request-token.provider.d.ts +0 -2
- package/authentication/server/authentication-api-request-token.provider.js +3 -11
- package/authentication/server/authentication.api-controller.d.ts +1 -2
- package/authentication/server/authentication.api-controller.js +8 -9
- package/authentication/server/authentication.audit.d.ts +3 -2
- package/authentication/server/authentication.service.d.ts +27 -1
- package/authentication/server/authentication.service.js +67 -18
- package/authentication/server/drizzle/{0000_normal_paper_doll.sql → 0000_soft_tag.sql} +25 -32
- package/authentication/server/drizzle/meta/0000_snapshot.json +180 -205
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/authentication/server/helper.js +9 -2
- package/authentication/server/module.d.ts +4 -1
- package/authentication/server/module.js +9 -5
- package/authentication/server/schemas.d.ts +2 -1
- package/authentication/server/schemas.js +2 -2
- package/authentication/server/subject.service.d.ts +17 -11
- package/authentication/server/subject.service.js +86 -84
- package/authentication/tests/authentication-ancillary.service.test.d.ts +1 -0
- package/authentication/tests/authentication-ancillary.service.test.js +13 -0
- package/authentication/tests/authentication-secret-requirements.validator.test.d.ts +1 -0
- package/authentication/tests/authentication-secret-requirements.validator.test.js +29 -0
- package/authentication/tests/authentication.api-controller.test.d.ts +1 -0
- package/authentication/tests/authentication.api-controller.test.js +88 -0
- package/authentication/tests/authentication.api-request-token.provider.test.d.ts +1 -0
- package/authentication/tests/authentication.api-request-token.provider.test.js +48 -0
- package/authentication/tests/authentication.client-middleware.test.d.ts +1 -0
- package/authentication/tests/authentication.client-middleware.test.js +23 -0
- package/authentication/tests/authentication.client-service.test.d.ts +1 -0
- package/authentication/tests/authentication.client-service.test.js +70 -0
- package/authentication/tests/authentication.service.test.d.ts +1 -0
- package/authentication/tests/authentication.service.test.js +186 -0
- package/authentication/tests/authentication.test-ancillary-service.d.ts +9 -0
- package/authentication/tests/authentication.test-ancillary-service.js +27 -0
- package/authentication/tests/helper.test.d.ts +1 -0
- package/authentication/tests/helper.test.js +107 -0
- package/authentication/tests/secret-requirements.error.test.d.ts +1 -0
- package/authentication/tests/secret-requirements.error.test.js +14 -0
- package/authentication/tests/subject.service.test.d.ts +1 -0
- package/authentication/tests/subject.service.test.js +140 -0
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +1 -1
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/postgres/module.d.ts +7 -1
- package/circuit-breaker/postgres/module.js +8 -6
- package/circuit-breaker/tests/circuit-breaker.test.js +2 -22
- package/document-management/api/document-management.api.js +2 -6
- package/document-management/server/services/document-validation.service.js +6 -5
- package/document-management/server/services/document-workflow.service.js +5 -5
- package/document-management/service-models/document-folders.view-model.d.ts +5 -2
- package/document-management/service-models/document-folders.view-model.js +42 -9
- package/document-management/service-models/enriched/enriched-document-management-data.view.js +1 -1
- package/examples/document-management/main.js +4 -4
- package/http/client/adapters/undici.adapter.d.ts +7 -5
- package/http/client/adapters/undici.adapter.js +13 -10
- package/http/client/module.d.ts +3 -1
- package/http/client/module.js +8 -9
- package/http/server/http-server.d.ts +2 -0
- package/http/server/node/module.d.ts +6 -2
- package/http/server/node/module.js +6 -4
- package/http/server/node/node-http-server.d.ts +2 -0
- package/http/server/node/node-http-server.js +7 -0
- package/http/types.d.ts +1 -1
- package/key-value-store/postgres/module.d.ts +7 -1
- package/key-value-store/postgres/module.js +7 -3
- package/lock/postgres/lock.js +0 -1
- package/lock/postgres/module.d.ts +7 -1
- package/lock/postgres/module.js +9 -5
- package/logger/formatter.d.ts +2 -0
- package/logger/formatters/json.js +2 -2
- package/logger/formatters/pretty-print.js +8 -10
- package/logger/logger.d.ts +1 -1
- package/logger/logger.js +15 -12
- package/message-bus/local/module.d.ts +5 -2
- package/message-bus/local/module.js +5 -4
- package/module/module.d.ts +2 -1
- package/module/module.js +3 -0
- package/module/modules/web-server.module.d.ts +11 -6
- package/module/modules/web-server.module.js +15 -10
- package/orm/decorators.d.ts +24 -1
- package/orm/decorators.js +40 -4
- package/orm/query/base.d.ts +17 -17
- package/orm/query/base.js +1 -1
- package/orm/repository.types.d.ts +45 -1
- package/orm/schemas/tsvector.js +1 -1
- package/orm/server/drizzle/schema-converter.d.ts +3 -1
- package/orm/server/drizzle/schema-converter.js +120 -14
- package/orm/server/index.d.ts +1 -0
- package/orm/server/index.js +1 -0
- package/orm/server/module.d.ts +4 -2
- package/orm/server/module.js +6 -5
- package/orm/server/query-converter.d.ts +6 -3
- package/orm/server/query-converter.js +32 -20
- package/orm/server/repository-config.d.ts +8 -0
- package/orm/server/repository-config.js +8 -0
- package/orm/server/repository.d.ts +117 -43
- package/orm/server/repository.js +757 -253
- package/orm/server/transaction.d.ts +4 -2
- package/orm/server/transaction.js +14 -5
- package/orm/server/transactional.d.ts +6 -2
- package/orm/server/transactional.js +39 -9
- package/orm/server/types.d.ts +2 -0
- package/orm/sqls/case-when.d.ts +3 -3
- package/orm/sqls/case-when.js +2 -2
- package/orm/sqls/sqls.d.ts +31 -5
- package/orm/sqls/sqls.js +69 -6
- package/orm/tests/data-types.test.d.ts +1 -0
- package/orm/tests/data-types.test.js +39 -0
- package/orm/tests/decorators.test.d.ts +1 -0
- package/orm/tests/decorators.test.js +77 -0
- package/orm/tests/encryption.test.d.ts +1 -0
- package/orm/tests/encryption.test.js +34 -0
- package/orm/tests/query-complex.test.d.ts +1 -0
- package/orm/tests/query-complex.test.js +203 -0
- package/orm/tests/query-converter-complex.test.d.ts +1 -0
- package/orm/tests/query-converter-complex.test.js +126 -0
- package/orm/tests/query-converter.test.d.ts +1 -0
- package/orm/tests/query-converter.test.js +123 -0
- package/orm/tests/repository-advanced.test.d.ts +1 -0
- package/orm/tests/repository-advanced.test.js +232 -0
- package/orm/tests/repository-attributes.test.d.ts +1 -0
- package/orm/tests/repository-attributes.test.js +99 -0
- package/orm/tests/repository-comprehensive.test.d.ts +1 -0
- package/orm/tests/repository-comprehensive.test.js +187 -0
- package/orm/tests/repository-coverage.test.d.ts +1 -0
- package/orm/tests/repository-coverage.test.js +303 -0
- package/orm/tests/repository-cti-complex.test.d.ts +1 -0
- package/orm/tests/repository-cti-complex.test.js +170 -0
- package/orm/tests/repository-cti-embedded.test.d.ts +1 -0
- package/orm/tests/repository-cti-embedded.test.js +188 -0
- package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
- package/orm/tests/repository-cti-extensive.test.js +308 -0
- package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
- package/orm/tests/repository-cti-mapping.test.js +121 -0
- package/orm/tests/repository-cti-search.test.d.ts +1 -0
- package/orm/tests/repository-cti-search.test.js +152 -0
- package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-cti-soft-delete.test.js +115 -0
- package/orm/tests/repository-cti-transactions.test.d.ts +1 -0
- package/orm/tests/repository-cti-transactions.test.js +126 -0
- package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
- package/orm/tests/repository-cti-upsert-many.test.js +127 -0
- package/orm/tests/repository-cti.test.d.ts +1 -0
- package/orm/tests/repository-cti.test.js +456 -0
- package/orm/tests/repository-edge-cases.test.d.ts +1 -0
- package/orm/tests/repository-edge-cases.test.js +216 -0
- package/orm/tests/repository-expiration.test.d.ts +1 -0
- package/orm/tests/repository-expiration.test.js +153 -0
- package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
- package/orm/tests/repository-extra-coverage.test.js +546 -0
- package/orm/tests/repository-mapping.test.d.ts +1 -0
- package/orm/tests/repository-mapping.test.js +71 -0
- package/orm/tests/repository-regression.test.d.ts +1 -0
- package/orm/tests/repository-regression.test.js +330 -0
- package/orm/tests/repository-search-coverage.test.d.ts +1 -0
- package/orm/tests/repository-search-coverage.test.js +129 -0
- package/orm/tests/repository-search.test.d.ts +1 -0
- package/orm/tests/repository-search.test.js +116 -0
- package/orm/tests/repository-soft-delete.test.d.ts +1 -0
- package/orm/tests/repository-soft-delete.test.js +143 -0
- package/orm/tests/repository-transactions-nested.test.d.ts +1 -0
- package/orm/tests/repository-transactions-nested.test.js +202 -0
- package/orm/tests/repository-types.test.d.ts +1 -0
- package/orm/tests/repository-types.test.js +218 -0
- package/orm/tests/schema-converter.test.d.ts +1 -0
- package/orm/tests/schema-converter.test.js +81 -0
- package/orm/tests/schema-generation.test.d.ts +1 -0
- package/orm/tests/schema-generation.test.js +127 -0
- package/orm/tests/sql-helpers.test.d.ts +1 -0
- package/orm/tests/sql-helpers.test.js +67 -0
- package/orm/tests/transaction-safety.test.d.ts +1 -0
- package/orm/tests/transaction-safety.test.js +81 -0
- package/orm/tests/transactional.test.d.ts +1 -0
- package/orm/tests/transactional.test.js +224 -0
- package/orm/tests/utils.test.d.ts +1 -0
- package/orm/tests/utils.test.js +70 -0
- package/orm/utils.d.ts +7 -0
- package/orm/utils.js +26 -6
- package/package.json +12 -7
- package/pool/pool.js +1 -1
- package/rate-limit/index.d.ts +2 -0
- package/rate-limit/index.js +2 -0
- package/rate-limit/postgres/drizzle/0000_watery_rage.sql +7 -0
- package/{queue → rate-limit}/postgres/drizzle/meta/0000_snapshot.json +14 -39
- package/rate-limit/postgres/drizzle/meta/_journal.json +13 -0
- package/{queue → rate-limit}/postgres/drizzle.config.js +1 -1
- package/rate-limit/postgres/index.d.ts +4 -0
- package/rate-limit/postgres/index.js +4 -0
- package/rate-limit/postgres/module.d.ts +12 -0
- package/rate-limit/postgres/module.js +28 -0
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +9 -0
- package/rate-limit/postgres/postgres-rate-limiter.js +56 -0
- package/rate-limit/postgres/rate-limit.model.d.ts +8 -0
- package/rate-limit/postgres/rate-limit.model.js +35 -0
- package/rate-limit/postgres/rate-limiter.provider.d.ts +6 -0
- package/rate-limit/postgres/rate-limiter.provider.js +21 -0
- package/rate-limit/postgres/schemas.d.ts +3 -0
- package/rate-limit/postgres/schemas.js +4 -0
- package/rate-limit/provider.d.ts +9 -0
- package/rate-limit/provider.js +2 -0
- package/rate-limit/rate-limiter.d.ts +35 -0
- package/rate-limit/rate-limiter.js +3 -0
- package/rate-limit/tests/postgres-rate-limiter.test.d.ts +1 -0
- package/rate-limit/tests/postgres-rate-limiter.test.js +92 -0
- package/signals/implementation/configure.d.ts +3 -0
- package/signals/implementation/configure.js +3 -0
- package/sse/data-stream-source.d.ts +1 -1
- package/sse/data-stream-source.js +6 -6
- package/task-queue/enqueue-batch.d.ts +17 -0
- package/task-queue/enqueue-batch.js +24 -0
- package/{queue → task-queue}/index.d.ts +1 -1
- package/{queue → task-queue}/index.js +1 -1
- package/task-queue/postgres/drizzle/0000_thin_black_panther.sql +74 -0
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +592 -0
- package/task-queue/postgres/drizzle/meta/_journal.json +13 -0
- package/task-queue/postgres/drizzle.config.d.ts +2 -0
- package/task-queue/postgres/drizzle.config.js +11 -0
- package/task-queue/postgres/index.d.ts +4 -0
- package/task-queue/postgres/index.js +4 -0
- package/task-queue/postgres/module.d.ts +12 -0
- package/task-queue/postgres/module.js +28 -0
- package/task-queue/postgres/schemas.d.ts +16 -0
- package/task-queue/postgres/schemas.js +8 -0
- package/task-queue/postgres/task-queue.d.ts +83 -0
- package/task-queue/postgres/task-queue.js +1054 -0
- package/task-queue/postgres/task-queue.provider.d.ts +7 -0
- package/{queue/postgres/queue.provider.js → task-queue/postgres/task-queue.provider.js} +8 -8
- package/task-queue/postgres/task.model.d.ts +39 -0
- package/task-queue/postgres/task.model.js +178 -0
- package/{queue → task-queue}/provider.d.ts +3 -3
- package/task-queue/provider.js +2 -0
- package/{queue → task-queue}/task-context.d.ts +7 -7
- package/{queue → task-queue}/task-context.js +8 -8
- package/{queue/queue.d.ts → task-queue/task-queue.d.ts} +128 -59
- package/task-queue/task-queue.js +200 -0
- package/task-queue/tests/complex.test.d.ts +1 -0
- package/task-queue/tests/complex.test.js +299 -0
- package/task-queue/tests/dependencies.test.d.ts +1 -0
- package/task-queue/tests/dependencies.test.js +174 -0
- package/task-queue/tests/queue.test.d.ts +1 -0
- package/task-queue/tests/queue.test.js +334 -0
- package/task-queue/tests/worker.test.d.ts +1 -0
- package/task-queue/tests/worker.test.js +163 -0
- package/test1.js +1 -1
- package/test4.js +2 -2
- package/unit-test/index.d.ts +1 -0
- package/unit-test/index.js +1 -0
- package/unit-test/integration-setup.d.ts +55 -0
- package/unit-test/integration-setup.js +182 -0
- package/utils/patterns.d.ts +3 -0
- package/utils/patterns.js +6 -1
- package/audit/drizzle/0001_previous_network.sql +0 -2
- package/audit/drizzle/meta/0001_snapshot.json +0 -195
- package/queue/enqueue-batch.d.ts +0 -17
- package/queue/enqueue-batch.js +0 -18
- package/queue/postgres/drizzle/0000_zippy_moondragon.sql +0 -11
- package/queue/postgres/drizzle/0001_certain_wild_pack.sql +0 -2
- package/queue/postgres/drizzle/0002_dear_meggan.sql +0 -2
- package/queue/postgres/drizzle/0003_tricky_venom.sql +0 -30
- package/queue/postgres/drizzle/meta/0001_snapshot.json +0 -103
- package/queue/postgres/drizzle/meta/0002_snapshot.json +0 -90
- package/queue/postgres/drizzle/meta/0003_snapshot.json +0 -288
- package/queue/postgres/drizzle/meta/_journal.json +0 -34
- package/queue/postgres/index.d.ts +0 -4
- package/queue/postgres/index.js +0 -4
- package/queue/postgres/module.d.ts +0 -9
- package/queue/postgres/module.js +0 -29
- package/queue/postgres/queue.d.ts +0 -60
- package/queue/postgres/queue.js +0 -681
- package/queue/postgres/queue.provider.d.ts +0 -7
- package/queue/postgres/schemas.d.ts +0 -14
- package/queue/postgres/schemas.js +0 -6
- package/queue/postgres/task.model.d.ts +0 -24
- package/queue/postgres/task.model.js +0 -115
- package/queue/provider.js +0 -2
- package/queue/queue.js +0 -131
- package/queue/tests/queue.test.js +0 -623
- package/test3.d.ts +0 -1
- package/test3.js +0 -47
- /package/{queue/tests/queue.test.d.ts → api/server/tests/csrf.middleware.test.d.ts} +0 -0
- /package/circuit-breaker/postgres/drizzle/{0000_hard_shocker.sql → 0000_cooing_korath.sql} +0 -0
- /package/{queue → rate-limit}/postgres/drizzle.config.d.ts +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
+
import { DependencyJoinMode, TaskQueueProvider, TaskState } from '../../task-queue/index.js';
|
|
4
|
+
import { setupIntegrationTest } from '../../unit-test/index.js';
|
|
5
|
+
import { timeout } from '../../utils/timing.js';
|
|
6
|
+
describe('Queue Dependencies & Tree Tests', () => {
|
|
7
|
+
let injector;
|
|
8
|
+
let queue;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
11
|
+
});
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
14
|
+
const queueName = `dep-queue-${Date.now()}-${Math.random()}`;
|
|
15
|
+
queue = queueProvider.get(queueName, {
|
|
16
|
+
visibilityTimeout: 1000,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await queue.clear();
|
|
21
|
+
});
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await injector?.dispose();
|
|
24
|
+
});
|
|
25
|
+
describe('Dependencies (Fan-In)', () => {
|
|
26
|
+
it('should schedule a task only after dependency completes (completeAfterTags)', async () => {
|
|
27
|
+
// 1. Create a dependent task (Waiting)
|
|
28
|
+
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
29
|
+
completeAfterTags: ['tag-a'],
|
|
30
|
+
});
|
|
31
|
+
expect(dependent.status).toBe(TaskState.Waiting);
|
|
32
|
+
// 2. Create the prerequisite task
|
|
33
|
+
const prereq = await queue.enqueue('prereq', { val: 1 }, { tags: ['tag-a'] });
|
|
34
|
+
// 3. Complete prereq
|
|
35
|
+
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
36
|
+
expect(dequeued?.id).toBe(prereq.id);
|
|
37
|
+
await queue.complete(dequeued);
|
|
38
|
+
await queue.processPendingFanIn();
|
|
39
|
+
const updatedDependent = await queue.getTask(dependent.id);
|
|
40
|
+
expect(updatedDependent?.status).toBe(TaskState.Completed);
|
|
41
|
+
});
|
|
42
|
+
it('should schedule a task to run after dependency completes (scheduleAfterTags)', async () => {
|
|
43
|
+
// 1. Dependent task
|
|
44
|
+
const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
|
|
45
|
+
scheduleAfterTags: ['tag-b'],
|
|
46
|
+
});
|
|
47
|
+
expect(dependent.status).toBe(TaskState.Waiting);
|
|
48
|
+
// 2. Prereq
|
|
49
|
+
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-b'] });
|
|
50
|
+
// 3. Complete prereq
|
|
51
|
+
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
52
|
+
await queue.complete(dequeued);
|
|
53
|
+
await queue.processPendingFanIn();
|
|
54
|
+
// 5. Dependent should be Pending (ready to run)
|
|
55
|
+
const updatedDependent = await queue.getTask(dependent.id);
|
|
56
|
+
expect(updatedDependent?.status).toBe(TaskState.Pending);
|
|
57
|
+
// 6. Should be dequeueable
|
|
58
|
+
const d2 = await queue.dequeue({ types: ['dependent'] });
|
|
59
|
+
expect(d2?.id).toBe(dependent.id);
|
|
60
|
+
});
|
|
61
|
+
it('should fail-fast if dependency fails', async () => {
|
|
62
|
+
const dependent = await queue.enqueue('dependent', {}, {
|
|
63
|
+
scheduleAfterTags: ['tag-fail'],
|
|
64
|
+
failFast: true
|
|
65
|
+
});
|
|
66
|
+
const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-fail'] });
|
|
67
|
+
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
68
|
+
// Fail fatally
|
|
69
|
+
await queue.fail(dequeued, new Error('boom'), true);
|
|
70
|
+
// Trigger resolution
|
|
71
|
+
await queue.processPendingFanIn();
|
|
72
|
+
let updatedDependent;
|
|
73
|
+
for (let i = 0; i < 20; i++) {
|
|
74
|
+
await timeout(100);
|
|
75
|
+
updatedDependent = await queue.getTask(dependent.id);
|
|
76
|
+
if (updatedDependent?.status === TaskState.Dead)
|
|
77
|
+
break;
|
|
78
|
+
await queue.processPendingFanIn(); // Retry processing if it didn't catch it yet
|
|
79
|
+
}
|
|
80
|
+
expect(updatedDependent?.status).toBe(TaskState.Dead);
|
|
81
|
+
expect(updatedDependent?.error?.code).toBe('DependencyFailed');
|
|
82
|
+
});
|
|
83
|
+
it('should respect DependencyJoinMode.Or', async () => {
|
|
84
|
+
// Wait for tag-1 OR tag-2
|
|
85
|
+
const dependent = await queue.enqueue('dependent', {}, {
|
|
86
|
+
scheduleAfterTags: ['tag-1', 'tag-2'],
|
|
87
|
+
dependencyJoinMode: DependencyJoinMode.Or
|
|
88
|
+
});
|
|
89
|
+
// Complete tag-1 only
|
|
90
|
+
await queue.enqueue('t1', {}, { tags: ['tag-1'] });
|
|
91
|
+
const d1 = await queue.dequeue({ types: ['t1'] });
|
|
92
|
+
await queue.complete(d1);
|
|
93
|
+
await queue.processPendingFanIn();
|
|
94
|
+
const updated = await queue.getTask(dependent.id);
|
|
95
|
+
expect(updated?.status).toBe(TaskState.Pending);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Tree Operations & Cancellation', () => {
|
|
99
|
+
it('should retrieve task tree and cancel hierarchy', async () => {
|
|
100
|
+
// Root
|
|
101
|
+
const root = await queue.enqueue('root', {});
|
|
102
|
+
// Child
|
|
103
|
+
const child = await queue.enqueue('child', {}, { parentId: root.id });
|
|
104
|
+
// Grandchild
|
|
105
|
+
const grandchild = await queue.enqueue('grandchild', {}, { parentId: child.id });
|
|
106
|
+
// Get Tree
|
|
107
|
+
const tree = await queue.getTree(root.id);
|
|
108
|
+
expect(tree.length).toBe(3);
|
|
109
|
+
expect(tree.map(t => t.id)).toContain(grandchild.id);
|
|
110
|
+
// Cancel Root
|
|
111
|
+
await queue.cancel(root.id);
|
|
112
|
+
// Verify all cancelled
|
|
113
|
+
const tRoot = await queue.getTask(root.id);
|
|
114
|
+
const tChild = await queue.getTask(child.id);
|
|
115
|
+
const tGrand = await queue.getTask(grandchild.id);
|
|
116
|
+
expect(tRoot?.status).toBe(TaskState.Cancelled);
|
|
117
|
+
expect(tChild?.status).toBe(TaskState.Cancelled);
|
|
118
|
+
expect(tGrand?.status).toBe(TaskState.Cancelled);
|
|
119
|
+
});
|
|
120
|
+
it('should cancel many by tags', async () => {
|
|
121
|
+
await queue.enqueue('t1', {}, { tags: ['group-a'] });
|
|
122
|
+
await queue.enqueue('t2', {}, { tags: ['group-a'] });
|
|
123
|
+
await queue.enqueue('t3', {}, { tags: ['group-b'] });
|
|
124
|
+
await queue.cancelManyByTags(['group-a']);
|
|
125
|
+
// Check status
|
|
126
|
+
const tasks = await queue.getManyByTags('group-a');
|
|
127
|
+
expect(tasks.length).toBe(2);
|
|
128
|
+
expect(tasks.every(t => t.status === TaskState.Cancelled)).toBe(true);
|
|
129
|
+
const tasksB = await queue.getManyByTags('group-b');
|
|
130
|
+
expect(tasksB[0]?.status).toBe(TaskState.Pending);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('Restart & Consumers', () => {
|
|
134
|
+
it('should restart a dead/cancelled task', async () => {
|
|
135
|
+
const task = await queue.enqueue('restart-test', {});
|
|
136
|
+
const d = await queue.dequeue({ types: ['restart-test'] });
|
|
137
|
+
await queue.fail(d, new Error('fatal'), true); // Dead
|
|
138
|
+
await queue.restart(task.id);
|
|
139
|
+
const updated = await queue.getTask(task.id);
|
|
140
|
+
expect(updated?.status).toBe(TaskState.Pending);
|
|
141
|
+
expect(updated?.tries).toBe(0);
|
|
142
|
+
expect(updated?.error).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
it('should consume tasks via async iterator (getConsumer)', async () => {
|
|
145
|
+
await queue.enqueue('c1', { val: 1 });
|
|
146
|
+
await queue.enqueue('c2', { val: 2 });
|
|
147
|
+
const token = new CancellationToken();
|
|
148
|
+
const consumer = queue.getConsumer(token);
|
|
149
|
+
const t1 = (await consumer.next()).value;
|
|
150
|
+
expect(t1.data.val).toBe(1);
|
|
151
|
+
const t2 = (await consumer.next()).value;
|
|
152
|
+
expect(t2.data.val).toBe(2);
|
|
153
|
+
token.set(); // Stop consumer
|
|
154
|
+
const t3 = await consumer.next();
|
|
155
|
+
expect(t3.done).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it('should consume batches via async iterator (getBatchConsumer)', async () => {
|
|
158
|
+
await queue.enqueueMany([
|
|
159
|
+
{ type: 'b', data: { val: 1 } },
|
|
160
|
+
{ type: 'b', data: { val: 2 } },
|
|
161
|
+
{ type: 'b', data: { val: 3 } }
|
|
162
|
+
]);
|
|
163
|
+
const token = new CancellationToken();
|
|
164
|
+
const batchConsumer = queue.getBatchConsumer(2, token);
|
|
165
|
+
const b1 = (await batchConsumer.next()).value;
|
|
166
|
+
expect(b1.length).toBe(2);
|
|
167
|
+
const b2 = (await batchConsumer.next()).value;
|
|
168
|
+
expect(b2.length).toBe(1);
|
|
169
|
+
token.set();
|
|
170
|
+
const b3 = await batchConsumer.next();
|
|
171
|
+
expect(b3.done).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { TaskQueueProvider, TaskState } from '../../task-queue/index.js';
|
|
3
|
+
import { setupIntegrationTest } from '../../unit-test/index.js';
|
|
4
|
+
import { currentTimestamp } from '../../utils/date-time.js';
|
|
5
|
+
import { timeout } from '../../utils/timing.js';
|
|
6
|
+
describe('Queue Integration Tests', () => {
|
|
7
|
+
let injector;
|
|
8
|
+
let queue;
|
|
9
|
+
const queueName = `test-queue-${Date.now()}`;
|
|
10
|
+
// Helper to verify state in DB
|
|
11
|
+
async function assertTaskState(id, state, message) {
|
|
12
|
+
const task = await queue.getTask(id);
|
|
13
|
+
expect(task?.status, message).toBe(state);
|
|
14
|
+
}
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
17
|
+
// Use the QueueProvider to Create/Retrieve the queue.
|
|
18
|
+
// This is the cleanest way to get a configured queue instance without
|
|
19
|
+
// messing with `injectArgument` manually in the test body.
|
|
20
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
21
|
+
queue = queueProvider.get(queueName, {
|
|
22
|
+
visibilityTimeout: 5000,
|
|
23
|
+
retryDelayMinimum: 100, // Fast retries for testing
|
|
24
|
+
retryDelayGrowth: 1, // Linear/No growth for predictable tests
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
// Drain the queue to prevent state leakage between tests
|
|
29
|
+
await queue.clear();
|
|
30
|
+
});
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
try {
|
|
33
|
+
await queue.clear();
|
|
34
|
+
await injector.dispose();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// Ignore known double-dispose issue from MessageBus interaction
|
|
38
|
+
if (error instanceof Error && error.message === 'MessageBus is disposed.') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it('Basic FIFO Flow', async () => {
|
|
45
|
+
const t1 = await queue.enqueue('test', { value: 'first' });
|
|
46
|
+
const t2 = await queue.enqueue('test', { value: 'second' });
|
|
47
|
+
const d1 = await queue.dequeue();
|
|
48
|
+
expect(d1?.id).toBe(t1.id);
|
|
49
|
+
expect(d1?.data.value).toBe('first');
|
|
50
|
+
await queue.complete(d1, { success: true });
|
|
51
|
+
await assertTaskState(t1.id, TaskState.Completed, 'Task 1 completed');
|
|
52
|
+
const d2 = await queue.dequeue();
|
|
53
|
+
expect(d2?.id).toBe(t2.id);
|
|
54
|
+
await queue.complete(d2, { success: true });
|
|
55
|
+
});
|
|
56
|
+
it('Priorities', async () => {
|
|
57
|
+
// Priority 1000 (default)
|
|
58
|
+
const low = await queue.enqueue('test', { value: 'low' });
|
|
59
|
+
// Priority 1
|
|
60
|
+
const high = await queue.enqueue('test', { value: 'high' }, { priority: 1 });
|
|
61
|
+
const first = await queue.dequeue();
|
|
62
|
+
expect(first?.id).toBe(high.id);
|
|
63
|
+
await queue.complete(first);
|
|
64
|
+
const second = await queue.dequeue();
|
|
65
|
+
expect(second?.id).toBe(low.id);
|
|
66
|
+
await queue.complete(second);
|
|
67
|
+
});
|
|
68
|
+
it('Deduplication (Idempotency Keys)', async () => {
|
|
69
|
+
const key = `unique-${Date.now()}`;
|
|
70
|
+
// 1. Initial Insert
|
|
71
|
+
const t1 = await queue.enqueue('test', { value: 'original' }, { idempotencyKey: key });
|
|
72
|
+
// 2. Default Strategy (replace: false): Should return existing task, ignore new data
|
|
73
|
+
const t2 = await queue.enqueue('test', { value: 'ignored' }, { idempotencyKey: key });
|
|
74
|
+
expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
|
|
75
|
+
const check1 = await queue.getTask(t1.id);
|
|
76
|
+
expect(check1?.data.value).toBe('original');
|
|
77
|
+
// 3. Replace Strategy: Should replace existing task with new data AND new ID
|
|
78
|
+
const t3 = await queue.enqueueMany([{ type: 'test', data: { value: 'updated' }, idempotencyKey: key }], { replace: true, returnTasks: true });
|
|
79
|
+
expect(t3[0].id, 'New ID if replaced').not.toBe(t1.id);
|
|
80
|
+
// New task should have new data
|
|
81
|
+
const checkNew = await queue.getTask(t3[0].id);
|
|
82
|
+
expect(checkNew?.data.value).toBe('updated');
|
|
83
|
+
expect(checkNew?.tries).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
it('Retries and Failures', async () => {
|
|
86
|
+
const task = await queue.enqueue('test', { value: 'fail-me' });
|
|
87
|
+
// Try 1
|
|
88
|
+
const attempt1 = await queue.dequeue();
|
|
89
|
+
expect(attempt1?.id).toBe(task.id);
|
|
90
|
+
await queue.fail(attempt1, { message: 'oops' });
|
|
91
|
+
// Force reschedule to now to bypass retryDelay
|
|
92
|
+
await queue.reschedule(task.id, Date.now());
|
|
93
|
+
// Try 2
|
|
94
|
+
const attempt2 = await queue.dequeue();
|
|
95
|
+
expect(attempt2?.id).toBe(task.id);
|
|
96
|
+
expect(attempt2?.tries).toBe(2);
|
|
97
|
+
// Fail fatally
|
|
98
|
+
await queue.fail(attempt2, { message: 'fatal error' }, true);
|
|
99
|
+
await assertTaskState(task.id, TaskState.Dead, 'Task is Dead after fatal error');
|
|
100
|
+
});
|
|
101
|
+
it('Hierarchy (Parent/Child)', async () => {
|
|
102
|
+
// A. Create Parent
|
|
103
|
+
const p = await queue.enqueue('test', { value: 'parent-manual' });
|
|
104
|
+
// B. Dequeue Parent
|
|
105
|
+
const pTask = await queue.dequeue();
|
|
106
|
+
expect(pTask?.id).toBe(p.id);
|
|
107
|
+
// C. Parent spawns child
|
|
108
|
+
const child = await queue.enqueue('test', { value: 'child-manual' }, { parentId: p.id });
|
|
109
|
+
// D. "Finish" Parent execution.
|
|
110
|
+
await queue.complete(pTask);
|
|
111
|
+
// await assertTaskState(p.id, TaskState.Waiting, 'Parent entered WAITING state'); // Depends on implementation details of auto-waiting
|
|
112
|
+
});
|
|
113
|
+
it('Batching', async () => {
|
|
114
|
+
const batch = queue.batch();
|
|
115
|
+
for (let i = 0; i < 5; i++) {
|
|
116
|
+
batch.add('test', { value: `batch-${i}` });
|
|
117
|
+
}
|
|
118
|
+
const tasks = await batch.enqueue(true);
|
|
119
|
+
expect(tasks.length).toBe(5);
|
|
120
|
+
const dequeuedBatch = await queue.dequeueMany(5);
|
|
121
|
+
expect(dequeuedBatch.length).toBe(5);
|
|
122
|
+
await queue.completeMany(dequeuedBatch);
|
|
123
|
+
const leftover = await queue.dequeue();
|
|
124
|
+
expect(leftover).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
128
|
+
let injector;
|
|
129
|
+
let queue;
|
|
130
|
+
beforeAll(async () => {
|
|
131
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
132
|
+
});
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
135
|
+
const queueName = `pg-test-queue-${Date.now()}-${Math.random()}`;
|
|
136
|
+
queue = queueProvider.get(queueName, {
|
|
137
|
+
visibilityTimeout: 200, // Short timeout for testing
|
|
138
|
+
retryDelayMinimum: 50,
|
|
139
|
+
retryDelayGrowth: 1,
|
|
140
|
+
circuitBreakerThreshold: 2,
|
|
141
|
+
circuitBreakerResetTimeout: 200,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
afterEach(async () => {
|
|
145
|
+
await queue.clear();
|
|
146
|
+
});
|
|
147
|
+
afterAll(async () => {
|
|
148
|
+
await injector?.dispose();
|
|
149
|
+
});
|
|
150
|
+
describe('Basic Lifecycle', () => {
|
|
151
|
+
it('should enqueue and dequeue a task', async () => {
|
|
152
|
+
await queue.enqueue('test', { foo: 'bar' });
|
|
153
|
+
const task = await queue.dequeue();
|
|
154
|
+
expect(task).toBeDefined();
|
|
155
|
+
expect(task?.data).toEqual({ foo: 'bar' });
|
|
156
|
+
expect(task?.status).toBe(TaskState.Running);
|
|
157
|
+
expect(task?.tries).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
it('should complete a task successfully', async () => {
|
|
160
|
+
const task = await queue.enqueue('test', { foo: 'bar' });
|
|
161
|
+
const dequeued = await queue.dequeue();
|
|
162
|
+
await queue.complete(dequeued, { result: true });
|
|
163
|
+
const updated = await queue.getTask(task.id);
|
|
164
|
+
expect(updated?.status).toBe(TaskState.Completed);
|
|
165
|
+
expect(updated?.result).toEqual({ result: true });
|
|
166
|
+
expect(updated?.completeTimestamp).toBeGreaterThan(0);
|
|
167
|
+
});
|
|
168
|
+
it('should fail a task and increment tries', async () => {
|
|
169
|
+
const task = await queue.enqueue('test', { foo: 'bar' });
|
|
170
|
+
const dequeued = await queue.dequeue();
|
|
171
|
+
await queue.fail(dequeued, new Error('temp failure'));
|
|
172
|
+
const updated = await queue.getTask(task.id);
|
|
173
|
+
expect(updated?.status).toBe(TaskState.Pending);
|
|
174
|
+
expect(updated?.tries).toBe(1);
|
|
175
|
+
expect(updated?.error).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('Priority and Scheduling', () => {
|
|
179
|
+
it('should dequeue tasks in priority order (lower number first)', async () => {
|
|
180
|
+
await queue.enqueue('test', { foo: 'low' }, { priority: 2000 });
|
|
181
|
+
await queue.enqueue('test', { foo: 'high' }, { priority: 10 });
|
|
182
|
+
await queue.enqueue('test', { foo: 'mid' }, { priority: 1000 });
|
|
183
|
+
const t1 = await queue.dequeue();
|
|
184
|
+
const t2 = await queue.dequeue();
|
|
185
|
+
const t3 = await queue.dequeue();
|
|
186
|
+
expect(t1?.data?.foo).toBe('high');
|
|
187
|
+
expect(t2?.data?.foo).toBe('mid');
|
|
188
|
+
expect(t3?.data?.foo).toBe('low');
|
|
189
|
+
});
|
|
190
|
+
it('should not dequeue a task scheduled in the future', async () => {
|
|
191
|
+
const future = currentTimestamp() + 500;
|
|
192
|
+
await queue.enqueue('test', { foo: 'future' }, { scheduleTimestamp: future });
|
|
193
|
+
const task = await queue.dequeue();
|
|
194
|
+
expect(task).toBeUndefined();
|
|
195
|
+
await timeout(600);
|
|
196
|
+
const taskLater = await queue.dequeue();
|
|
197
|
+
expect(taskLater).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('Concurrency Control', () => {
|
|
201
|
+
it('should respect global concurrency limits', async () => {
|
|
202
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
203
|
+
const limitedQueue = queueProvider.get(`limit-test-${Date.now()}`, { globalConcurrency: 2 });
|
|
204
|
+
await limitedQueue.enqueueMany([
|
|
205
|
+
{ type: 'test', data: { foo: '1' } },
|
|
206
|
+
{ type: 'test', data: { foo: '2' } },
|
|
207
|
+
{ type: 'test', data: { foo: '3' } },
|
|
208
|
+
]);
|
|
209
|
+
const t1 = await limitedQueue.dequeue();
|
|
210
|
+
const t2 = await limitedQueue.dequeue();
|
|
211
|
+
const t3 = await limitedQueue.dequeue();
|
|
212
|
+
expect(t1).toBeDefined();
|
|
213
|
+
expect(t2).toBeDefined();
|
|
214
|
+
expect(t3).toBeUndefined(); // Limit reached
|
|
215
|
+
await limitedQueue.complete(t1);
|
|
216
|
+
const t3Retry = await limitedQueue.dequeue();
|
|
217
|
+
expect(t3Retry).toBeDefined(); // Slot opened
|
|
218
|
+
await limitedQueue.clear();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('Circuit Breaker', () => {
|
|
222
|
+
it('should trip the breaker after threshold failures', async () => {
|
|
223
|
+
// Config: circuitBreakerThreshold: 2 (set in beforeEach)
|
|
224
|
+
await queue.enqueue('test', { foo: '1' });
|
|
225
|
+
await queue.enqueue('test', { foo: '2' });
|
|
226
|
+
await queue.enqueue('test', { foo: '3' });
|
|
227
|
+
await queue.fail((await queue.dequeue()), 'err');
|
|
228
|
+
await queue.fail((await queue.dequeue()), 'err');
|
|
229
|
+
// Breaker should be Open
|
|
230
|
+
const t3Attempt = await queue.dequeue();
|
|
231
|
+
expect(t3Attempt).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
it('should allow a single probe in Half-Open state', async () => {
|
|
234
|
+
await queue.enqueueMany([
|
|
235
|
+
{ type: 'test', data: { foo: '1' } },
|
|
236
|
+
{ type: 'test', data: { foo: '2' } },
|
|
237
|
+
]);
|
|
238
|
+
await queue.fail((await queue.dequeue()), 'err');
|
|
239
|
+
await queue.fail((await queue.dequeue()), 'err');
|
|
240
|
+
// Breaker is Open. Wait for reset timeout (200ms)
|
|
241
|
+
await timeout(250);
|
|
242
|
+
const probe = await queue.dequeue();
|
|
243
|
+
expect(probe).toBeDefined();
|
|
244
|
+
const secondAttempt = await queue.dequeue();
|
|
245
|
+
expect(secondAttempt).toBeUndefined(); // Only 1 probe allowed in Half-Open
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('Timeouts and Maintenance (Pruning)', () => {
|
|
249
|
+
it('should recover "Zombie" tasks (crashed workers)', async () => {
|
|
250
|
+
const task = await queue.enqueue('test', { foo: 'zombie' });
|
|
251
|
+
await queue.dequeue(); // Task is now Running with a token
|
|
252
|
+
// processTimeout is 200ms. Wait for it to expire.
|
|
253
|
+
await timeout(300);
|
|
254
|
+
await queue.maintenance();
|
|
255
|
+
const recovered = await queue.getTask(task.id);
|
|
256
|
+
expect(recovered?.status).toBe(TaskState.Pending);
|
|
257
|
+
expect(recovered?.tries).toBe(1);
|
|
258
|
+
expect(recovered?.token).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
it('should fail tasks that exceed Hard Execution Timeout via prune', async () => {
|
|
261
|
+
// Re-configure queue with very short execution timeout
|
|
262
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
263
|
+
const shortQueue = queueProvider.get(`prune-test-${Date.now()}`, { maxExecutionTime: 100 });
|
|
264
|
+
const task = await shortQueue.enqueue('test', { foo: 'long-running' });
|
|
265
|
+
await shortQueue.dequeue();
|
|
266
|
+
await timeout(200);
|
|
267
|
+
await shortQueue.maintenance();
|
|
268
|
+
const updated = await shortQueue.getTask(task.id);
|
|
269
|
+
expect(updated?.status).toBe(TaskState.Dead);
|
|
270
|
+
expect(updated?.error?.message).toContain('Hard Execution Timeout');
|
|
271
|
+
await shortQueue.clear();
|
|
272
|
+
});
|
|
273
|
+
it('should touch a task to extend token', async () => {
|
|
274
|
+
const task = await queue.enqueue('test', { foo: 'work' });
|
|
275
|
+
const dequeued = await queue.dequeue();
|
|
276
|
+
const initialLock = dequeued.visibilityDeadline;
|
|
277
|
+
await timeout(50);
|
|
278
|
+
const touched = await queue.touch(dequeued);
|
|
279
|
+
expect(touched?.visibilityDeadline).toBeGreaterThan(initialLock);
|
|
280
|
+
});
|
|
281
|
+
it('should prevent touching if token is lost (stolen by another worker)', async () => {
|
|
282
|
+
await queue.enqueue('test', { foo: 'work' });
|
|
283
|
+
const dequeued = await queue.dequeue();
|
|
284
|
+
expect(dequeued).toBeDefined();
|
|
285
|
+
// processTimeout is 200ms. Wait for it to expire.
|
|
286
|
+
await timeout(300);
|
|
287
|
+
await queue.maintenance();
|
|
288
|
+
await queue.dequeue(); // Stolen by another worker (tries=2)
|
|
289
|
+
// Original worker tries to touch
|
|
290
|
+
const touchResult = await queue.touch(dequeued);
|
|
291
|
+
expect(touchResult).toBeUndefined();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
describe('Batch Operations', () => {
|
|
295
|
+
it('should complete many tasks efficiently', async () => {
|
|
296
|
+
const tasks = await queue.enqueueMany([
|
|
297
|
+
{ type: 'test', data: { foo: '1' } },
|
|
298
|
+
{ type: 'test', data: { foo: '2' } },
|
|
299
|
+
], { returnTasks: true });
|
|
300
|
+
const d1 = await queue.dequeue();
|
|
301
|
+
const d2 = await queue.dequeue();
|
|
302
|
+
await queue.completeMany([d1, d2]);
|
|
303
|
+
const t1 = await queue.getTask(tasks[0].id);
|
|
304
|
+
const t2 = await queue.getTask(tasks[1].id);
|
|
305
|
+
expect(t1?.status).toBe(TaskState.Completed);
|
|
306
|
+
expect(t2?.status).toBe(TaskState.Completed);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('Rescheduling', () => {
|
|
310
|
+
it('should reschedule and refund tries if running', async () => {
|
|
311
|
+
const task = await queue.enqueue('test', { foo: 'reschedule-me' });
|
|
312
|
+
const dequeued = await queue.dequeue();
|
|
313
|
+
expect(dequeued?.tries).toBe(1);
|
|
314
|
+
const inFuture = currentTimestamp() + 1000;
|
|
315
|
+
await queue.reschedule(dequeued.id, inFuture);
|
|
316
|
+
const updated = await queue.getTask(task.id);
|
|
317
|
+
expect(updated?.status).toBe(TaskState.Pending);
|
|
318
|
+
expect(updated?.tries).toBe(0); // Refunded
|
|
319
|
+
expect(updated?.scheduleTimestamp).toBe(inFuture);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
describe('TaskContext (Worker DX)', () => {
|
|
323
|
+
it('checkpoint() should update progress and handle token loss', async () => {
|
|
324
|
+
const task = await queue.enqueue('test', { foo: 'progress' });
|
|
325
|
+
const dequeued = await queue.dequeue();
|
|
326
|
+
// In real scenarios TaskContext wraps the queue logic.
|
|
327
|
+
// Here we just verify touch/checkpoint effects on the DB.
|
|
328
|
+
await queue.touch(dequeued, { progress: 0.5, state: { step: 1 } });
|
|
329
|
+
const updated = await queue.getTask(task.id);
|
|
330
|
+
expect(updated?.progress).toBe(0.5);
|
|
331
|
+
expect(updated?.state).toEqual({ step: 1 });
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
+
import { TaskQueueProvider, TaskResult, TaskState } from '../../task-queue/index.js';
|
|
4
|
+
import { setupIntegrationTest } from '../../unit-test/index.js';
|
|
5
|
+
import { timeout } from '../../utils/timing.js';
|
|
6
|
+
describe('Worker & Base Class Tests', () => {
|
|
7
|
+
let injector;
|
|
8
|
+
let queue;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
11
|
+
});
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
14
|
+
const queueName = `worker-queue-${Date.now()}-${Math.random()}`;
|
|
15
|
+
queue = queueProvider.get(queueName, {
|
|
16
|
+
visibilityTimeout: 500, // Short visibility for testing lease loss
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await queue.clear();
|
|
21
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
22
|
+
await queueProvider.get('other-queue').clear();
|
|
23
|
+
});
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await injector?.dispose();
|
|
26
|
+
});
|
|
27
|
+
it('should process tasks using process() helper', async () => {
|
|
28
|
+
const t1 = await queue.enqueue('work', { val: 1 });
|
|
29
|
+
const t2 = await queue.enqueue('work', { val: 2 });
|
|
30
|
+
const processed = [];
|
|
31
|
+
const token = new CancellationToken();
|
|
32
|
+
queue.process({ cancellationSignal: token }, async (context) => {
|
|
33
|
+
processed.push(context.data.val);
|
|
34
|
+
return TaskResult.Complete();
|
|
35
|
+
});
|
|
36
|
+
// Wait until 2 tasks are processed
|
|
37
|
+
for (let i = 0; i < 20; i++) {
|
|
38
|
+
if (processed.length === 2)
|
|
39
|
+
break;
|
|
40
|
+
await timeout(100);
|
|
41
|
+
}
|
|
42
|
+
token.set(); // Stop worker
|
|
43
|
+
expect(processed).toContain(1);
|
|
44
|
+
expect(processed).toContain(2);
|
|
45
|
+
expect(processed.length).toBe(2);
|
|
46
|
+
const check1 = await queue.getTask(t1.id);
|
|
47
|
+
const check2 = await queue.getTask(t2.id);
|
|
48
|
+
expect(check1?.status).toBe(TaskState.Completed);
|
|
49
|
+
expect(check2?.status).toBe(TaskState.Completed);
|
|
50
|
+
});
|
|
51
|
+
it('should handle errors in worker gracefully', async () => {
|
|
52
|
+
const task = await queue.enqueue('fail', {});
|
|
53
|
+
const token = new CancellationToken();
|
|
54
|
+
queue.process({ cancellationSignal: token }, async () => {
|
|
55
|
+
throw new Error('worker error');
|
|
56
|
+
});
|
|
57
|
+
await timeout(200);
|
|
58
|
+
token.set();
|
|
59
|
+
const updated = await queue.getTask(task.id);
|
|
60
|
+
expect(updated?.status).toBe(TaskState.Pending); // Should retry
|
|
61
|
+
expect(updated?.tries).toBe(1);
|
|
62
|
+
expect(updated?.error?.message).toBe('worker error');
|
|
63
|
+
});
|
|
64
|
+
it('should process batch of tasks', async () => {
|
|
65
|
+
await queue.enqueueMany([
|
|
66
|
+
{ type: 'batch', data: { v: 1 } },
|
|
67
|
+
{ type: 'batch', data: { v: 2 } },
|
|
68
|
+
{ type: 'batch', data: { v: 3 } },
|
|
69
|
+
]);
|
|
70
|
+
const processedBatch = [];
|
|
71
|
+
const token = new CancellationToken();
|
|
72
|
+
queue.processBatch({ batchSize: 2, cancellationSignal: token }, async (context) => {
|
|
73
|
+
expect(context.tasks.length).toBeLessThanOrEqual(2);
|
|
74
|
+
context.tasks.forEach(t => processedBatch.push(t.data.v));
|
|
75
|
+
return context.tasks.map(() => TaskResult.Complete());
|
|
76
|
+
});
|
|
77
|
+
for (let i = 0; i < 20; i++) {
|
|
78
|
+
if (processedBatch.length === 3)
|
|
79
|
+
break;
|
|
80
|
+
await timeout(100);
|
|
81
|
+
}
|
|
82
|
+
token.set();
|
|
83
|
+
expect(processedBatch.sort()).toEqual([1, 2, 3]);
|
|
84
|
+
});
|
|
85
|
+
it('should extend lease (heartbeat) during long processing', async () => {
|
|
86
|
+
const task = await queue.enqueue('long', {});
|
|
87
|
+
const token = new CancellationToken();
|
|
88
|
+
let executed = false;
|
|
89
|
+
queue.process({ cancellationSignal: token }, async (_context) => {
|
|
90
|
+
// Simulate long work > visibilityTimeout (500ms)
|
|
91
|
+
await timeout(700);
|
|
92
|
+
executed = true;
|
|
93
|
+
return TaskResult.Complete();
|
|
94
|
+
});
|
|
95
|
+
await timeout(1000);
|
|
96
|
+
token.set();
|
|
97
|
+
expect(executed).toBe(true);
|
|
98
|
+
const updated = await queue.getTask(task.id);
|
|
99
|
+
expect(updated?.status).toBe(TaskState.Completed);
|
|
100
|
+
});
|
|
101
|
+
it('should handle TaskResult actions (Fail, Reschedule)', async () => {
|
|
102
|
+
const tFail = await queue.enqueue('fail-action', {});
|
|
103
|
+
const tResched = await queue.enqueue('resched-action', {});
|
|
104
|
+
const token = new CancellationToken();
|
|
105
|
+
const processed = new Set();
|
|
106
|
+
queue.process({ cancellationSignal: token }, async (context) => {
|
|
107
|
+
processed.add(context.id);
|
|
108
|
+
if (context.id === tFail.id) {
|
|
109
|
+
return TaskResult.Fail(new Error('explicit fail'));
|
|
110
|
+
}
|
|
111
|
+
if (context.id === tResched.id) {
|
|
112
|
+
return TaskResult.RescheduleBy(1000);
|
|
113
|
+
}
|
|
114
|
+
return TaskResult.Complete();
|
|
115
|
+
});
|
|
116
|
+
for (let i = 0; i < 20; i++) {
|
|
117
|
+
if (processed.size === 2)
|
|
118
|
+
break;
|
|
119
|
+
await timeout(100);
|
|
120
|
+
}
|
|
121
|
+
token.set();
|
|
122
|
+
const uFail = await queue.getTask(tFail.id);
|
|
123
|
+
expect(uFail?.status).toBe(TaskState.Pending); // Retry
|
|
124
|
+
expect(uFail?.error?.message).toBe('explicit fail');
|
|
125
|
+
const uResched = await queue.getTask(tResched.id);
|
|
126
|
+
expect(uResched?.status).toBe(TaskState.Pending);
|
|
127
|
+
expect(uResched?.scheduleTimestamp).toBeGreaterThan(Date.now());
|
|
128
|
+
});
|
|
129
|
+
it('should exercise TaskContext methods', async () => {
|
|
130
|
+
const task = await queue.enqueue('context-test', { val: 1 });
|
|
131
|
+
const token = new CancellationToken();
|
|
132
|
+
let executed = false;
|
|
133
|
+
queue.process({ cancellationSignal: token, types: ['context-test'] }, async (context) => {
|
|
134
|
+
expect(context.id).toBe(task.id);
|
|
135
|
+
expect(context.data).toEqual({ val: 1 });
|
|
136
|
+
expect(context.attempt).toBe(1);
|
|
137
|
+
expect(context.triesLeft).toBeGreaterThan(0);
|
|
138
|
+
expect(context.logger).toBeDefined();
|
|
139
|
+
expect(context.signal).toBeDefined();
|
|
140
|
+
await context.checkpoint({ progress: 0.5 });
|
|
141
|
+
const child = await context.spawn('child', { c: 1 });
|
|
142
|
+
expect(child.parentId).toBe(task.id);
|
|
143
|
+
const children = await context.spawnMany([{ type: 'child', data: { c: 2 } }]);
|
|
144
|
+
expect(children[0]?.parentId).toBe(task.id);
|
|
145
|
+
// Other queue spawn
|
|
146
|
+
const otherQueue = injector.resolve(TaskQueueProvider).get('other-queue');
|
|
147
|
+
const otherChild = await context.spawn(otherQueue, 'other', { x: 1 });
|
|
148
|
+
expect(otherChild.parentId).toBe(task.id);
|
|
149
|
+
expect(otherChild.namespace).toBe('other-queue');
|
|
150
|
+
const otherChildren = await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
|
|
151
|
+
expect(otherChildren[0]?.parentId).toBe(task.id);
|
|
152
|
+
executed = true;
|
|
153
|
+
return TaskResult.Complete();
|
|
154
|
+
});
|
|
155
|
+
for (let i = 0; i < 20; i++) {
|
|
156
|
+
if (executed)
|
|
157
|
+
break;
|
|
158
|
+
await timeout(100);
|
|
159
|
+
}
|
|
160
|
+
token.set();
|
|
161
|
+
expect(executed).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|