@tstdl/base 0.93.182 → 0.93.184
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/api/server/api-request-token.provider.js +1 -1
- package/api/server/gateway.js +6 -1
- package/authentication/authentication.api.d.ts +13 -40
- package/authentication/authentication.api.js +5 -14
- package/authentication/client/authentication.service.d.ts +6 -14
- package/authentication/client/authentication.service.js +22 -4
- package/authentication/client/module.d.ts +1 -1
- package/authentication/client/module.js +4 -4
- package/authentication/models/index.d.ts +1 -0
- package/authentication/models/index.js +1 -0
- package/authentication/models/totp-results.model.d.ts +11 -0
- package/authentication/models/totp-results.model.js +37 -0
- package/authentication/server/authentication.api-controller.d.ts +3 -3
- package/authentication/server/authentication.api-controller.js +31 -4
- package/authentication/server/authentication.service.d.ts +5 -14
- package/authentication/server/authentication.service.js +6 -4
- package/core.d.ts +0 -5
- package/core.js +0 -8
- package/document-management/api/document-management.api.d.ts +2 -2
- package/document-management/service-models/document.service-model.d.ts +1 -1
- package/examples/config.d.ts +25 -0
- package/examples/config.js +26 -0
- package/notification/server/module.d.ts +1 -1
- package/notification/server/module.js +1 -1
- package/package.json +5 -5
- package/signals/api.d.ts +5 -1
- package/signals/api.js +3 -1
- package/signals/implementation/api.d.ts +10 -1
- package/signals/implementation/api.js +7 -1
- package/signals/implementation/asserts.d.ts +2 -2
- package/signals/implementation/asserts.js +3 -3
- package/signals/implementation/computed.d.ts +7 -34
- package/signals/implementation/computed.js +14 -83
- package/signals/implementation/configure.js +6 -2
- package/signals/implementation/effect.d.ts +65 -46
- package/signals/implementation/effect.js +97 -62
- package/signals/implementation/index.d.ts +2 -4
- package/signals/implementation/index.js +2 -4
- package/signals/implementation/linked_signal.d.ts +36 -0
- package/signals/implementation/linked_signal.js +34 -0
- package/signals/implementation/primitive/computed.d.ts +55 -0
- package/signals/implementation/primitive/computed.js +107 -0
- package/signals/implementation/primitive/effect.d.ts +26 -0
- package/signals/implementation/primitive/effect.js +31 -0
- package/signals/implementation/{equality.d.ts → primitive/equality.d.ts} +1 -1
- package/signals/implementation/{equality.js → primitive/equality.js} +1 -1
- package/signals/implementation/primitive/errors.d.ts +10 -0
- package/signals/implementation/{errors.js → primitive/errors.js} +3 -4
- package/signals/implementation/primitive/formatter.d.ts +19 -0
- package/signals/implementation/primitive/formatter.js +136 -0
- package/signals/implementation/{graph.d.ts → primitive/graph.d.ts} +68 -36
- package/signals/implementation/primitive/graph.js +386 -0
- package/signals/implementation/primitive/linked_signal.d.ts +46 -0
- package/signals/implementation/primitive/linked_signal.js +110 -0
- package/signals/implementation/primitive/signal.d.ts +31 -0
- package/signals/implementation/primitive/signal.js +80 -0
- package/signals/implementation/primitive/untracked.d.ts +12 -0
- package/signals/implementation/primitive/untracked.js +23 -0
- package/signals/implementation/{watch.d.ts → primitive/watch.d.ts} +1 -2
- package/signals/implementation/{watch.js → primitive/watch.js} +22 -16
- package/signals/implementation/resource/api.d.ts +275 -0
- package/signals/implementation/resource/api.js +26 -0
- package/signals/implementation/resource/debounce.d.ts +13 -0
- package/signals/implementation/resource/debounce.js +113 -0
- package/signals/implementation/resource/from_snapshots.d.ts +16 -0
- package/signals/implementation/resource/from_snapshots.js +44 -0
- package/signals/implementation/resource/index.d.ts +11 -0
- package/signals/implementation/resource/index.js +11 -0
- package/signals/implementation/resource/resource.d.ts +110 -0
- package/signals/implementation/resource/resource.js +402 -0
- package/signals/implementation/root_effect_scheduler.d.ts +50 -0
- package/signals/implementation/root_effect_scheduler.js +66 -0
- package/signals/implementation/signal.d.ts +42 -18
- package/signals/implementation/signal.js +29 -49
- package/signals/implementation/to-observable.d.ts +12 -5
- package/signals/implementation/to-observable.js +12 -2
- package/signals/implementation/to-signal.d.ts +9 -18
- package/signals/implementation/to-signal.js +46 -13
- package/signals/implementation/untracked.d.ts +1 -1
- package/signals/implementation/untracked.js +3 -11
- package/signals/operators/debounce.d.ts +8 -0
- package/signals/operators/debounce.js +19 -0
- package/signals/operators/derive-async.js +43 -15
- package/signals/operators/index.d.ts +2 -0
- package/signals/operators/index.js +2 -0
- package/signals/operators/throttle.d.ts +8 -0
- package/signals/operators/throttle.js +31 -0
- package/ai/genkit/tests/multi-region.test.d.ts +0 -2
- package/ai/genkit/tests/multi-region.test.js +0 -179
- package/ai/genkit/tests/token-limit-fallback.test.d.ts +0 -2
- package/ai/genkit/tests/token-limit-fallback.test.js +0 -209
- package/ai/prompts/tests/prompt-builder.test.d.ts +0 -1
- package/ai/prompts/tests/prompt-builder.test.js +0 -22
- package/ai/tests/instructions-formatter.test.d.ts +0 -1
- package/ai/tests/instructions-formatter.test.js +0 -116
- package/ai/tests/steering.test.d.ts +0 -1
- package/ai/tests/steering.test.js +0 -37
- package/api/client/tests/api-client.test.d.ts +0 -1
- package/api/client/tests/api-client.test.js +0 -194
- package/api/server/tests/csrf.middleware.test.d.ts +0 -1
- package/api/server/tests/csrf.middleware.test.js +0 -91
- package/authentication/tests/authentication-password-requirements.validator.test.d.ts +0 -1
- package/authentication/tests/authentication-password-requirements.validator.test.js +0 -29
- package/authentication/tests/authentication.api-controller.test.d.ts +0 -1
- package/authentication/tests/authentication.api-controller.test.js +0 -156
- package/authentication/tests/authentication.api-request-token.provider.test.d.ts +0 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +0 -48
- package/authentication/tests/authentication.client-error-handling.test.d.ts +0 -1
- package/authentication/tests/authentication.client-error-handling.test.js +0 -123
- package/authentication/tests/authentication.client-middleware.test.d.ts +0 -1
- package/authentication/tests/authentication.client-middleware.test.js +0 -118
- package/authentication/tests/authentication.client-service-methods.test.d.ts +0 -1
- package/authentication/tests/authentication.client-service-methods.test.js +0 -177
- package/authentication/tests/authentication.client-service-refresh.test.d.ts +0 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +0 -153
- package/authentication/tests/authentication.client-service.test.d.ts +0 -1
- package/authentication/tests/authentication.client-service.test.js +0 -76
- package/authentication/tests/authentication.refresh-busy-loop.test.d.ts +0 -1
- package/authentication/tests/authentication.refresh-busy-loop.test.js +0 -84
- package/authentication/tests/authentication.service.test.d.ts +0 -1
- package/authentication/tests/authentication.service.test.js +0 -167
- package/authentication/tests/authentication.test-ancillary-service.d.ts +0 -9
- package/authentication/tests/authentication.test-ancillary-service.js +0 -27
- package/authentication/tests/brute-force-protection.test.d.ts +0 -1
- package/authentication/tests/brute-force-protection.test.js +0 -211
- package/authentication/tests/helper.test.d.ts +0 -1
- package/authentication/tests/helper.test.js +0 -122
- package/authentication/tests/password-requirements.error.test.d.ts +0 -1
- package/authentication/tests/password-requirements.error.test.js +0 -14
- package/authentication/tests/remember.api.test.d.ts +0 -1
- package/authentication/tests/remember.api.test.js +0 -117
- package/authentication/tests/remember.service.test.d.ts +0 -1
- package/authentication/tests/remember.service.test.js +0 -83
- package/authentication/tests/subject.service.test.d.ts +0 -1
- package/authentication/tests/subject.service.test.js +0 -140
- package/authentication/tests/suspended-subject.test.d.ts +0 -1
- package/authentication/tests/suspended-subject.test.js +0 -120
- package/authentication/tests/totp.enrollment.test.d.ts +0 -1
- package/authentication/tests/totp.enrollment.test.js +0 -123
- package/authentication/tests/totp.login.test.d.ts +0 -1
- package/authentication/tests/totp.login.test.js +0 -213
- package/authentication/tests/totp.recovery-codes.test.d.ts +0 -1
- package/authentication/tests/totp.recovery-codes.test.js +0 -97
- package/authentication/tests/totp.status.test.d.ts +0 -1
- package/authentication/tests/totp.status.test.js +0 -72
- package/cancellation/tests/coverage.test.d.ts +0 -1
- package/cancellation/tests/coverage.test.js +0 -49
- package/cancellation/tests/leak.test.d.ts +0 -1
- package/cancellation/tests/leak.test.js +0 -35
- package/cancellation/tests/token.test.d.ts +0 -1
- package/cancellation/tests/token.test.js +0 -136
- package/circuit-breaker/tests/circuit-breaker.test.d.ts +0 -1
- package/circuit-breaker/tests/circuit-breaker.test.js +0 -116
- package/cryptography/tests/cryptography.test.d.ts +0 -1
- package/cryptography/tests/cryptography.test.js +0 -175
- package/cryptography/tests/jwt.test.d.ts +0 -1
- package/cryptography/tests/jwt.test.js +0 -54
- package/cryptography/tests/modern.test.d.ts +0 -1
- package/cryptography/tests/modern.test.js +0 -105
- package/cryptography/tests/module.test.d.ts +0 -1
- package/cryptography/tests/module.test.js +0 -100
- package/cryptography/tests/totp.test.d.ts +0 -1
- package/cryptography/tests/totp.test.js +0 -108
- package/document-management/tests/ai-config-hierarchy.test.d.ts +0 -1
- package/document-management/tests/ai-config-hierarchy.test.js +0 -59
- package/document-management/tests/ai-config-integration.test.d.ts +0 -1
- package/document-management/tests/ai-config-integration.test.js +0 -125
- package/document-management/tests/ai-config-merge.test.d.ts +0 -1
- package/document-management/tests/ai-config-merge.test.js +0 -46
- package/document-management/tests/document-management-ai-overrides.test.d.ts +0 -1
- package/document-management/tests/document-management-ai-overrides.test.js +0 -63
- package/document-management/tests/document-management-core.test.d.ts +0 -1
- package/document-management/tests/document-management-core.test.js +0 -157
- package/document-management/tests/document-management.api.test.d.ts +0 -1
- package/document-management/tests/document-management.api.test.js +0 -101
- package/document-management/tests/document-statistics.service.test.d.ts +0 -1
- package/document-management/tests/document-statistics.service.test.js +0 -498
- package/document-management/tests/document-validation-ai-overrides.test.d.ts +0 -1
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -87
- package/document-management/tests/document.service.test.d.ts +0 -1
- package/document-management/tests/document.service.test.js +0 -143
- package/document-management/tests/enum-helpers.test.d.ts +0 -1
- package/document-management/tests/enum-helpers.test.js +0 -452
- package/document-management/tests/helper.d.ts +0 -24
- package/document-management/tests/helper.js +0 -39
- package/errors/tests/format.test.d.ts +0 -1
- package/errors/tests/format.test.js +0 -84
- package/http/tests/server-timing.test.d.ts +0 -1
- package/http/tests/server-timing.test.js +0 -42
- package/injector/tests/advanced.test.d.ts +0 -1
- package/injector/tests/advanced.test.js +0 -116
- package/injector/tests/async-init.test.d.ts +0 -1
- package/injector/tests/async-init.test.js +0 -77
- package/injector/tests/basic.test.d.ts +0 -1
- package/injector/tests/basic.test.js +0 -114
- package/injector/tests/hierarchical.test.d.ts +0 -1
- package/injector/tests/hierarchical.test.js +0 -59
- package/injector/tests/leak.test.d.ts +0 -1
- package/injector/tests/leak.test.js +0 -45
- package/injector/tests/lifecycles.test.d.ts +0 -1
- package/injector/tests/lifecycles.test.js +0 -109
- package/logger/tests/pretty-print.test.d.ts +0 -1
- package/logger/tests/pretty-print.test.js +0 -60
- package/notification/tests/notification-api.test.d.ts +0 -1
- package/notification/tests/notification-api.test.js +0 -124
- package/notification/tests/notification-client.test.d.ts +0 -1
- package/notification/tests/notification-client.test.js +0 -101
- package/notification/tests/notification-flow.test.d.ts +0 -1
- package/notification/tests/notification-flow.test.js +0 -296
- package/notification/tests/notification-sse.service.test.d.ts +0 -1
- package/notification/tests/notification-sse.service.test.js +0 -43
- package/notification/tests/notification-type.service.test.d.ts +0 -1
- package/notification/tests/notification-type.service.test.js +0 -41
- package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +0 -1
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +0 -303
- package/orm/tests/build-jsonb.test.d.ts +0 -1
- package/orm/tests/build-jsonb.test.js +0 -39
- package/orm/tests/data-types.test.d.ts +0 -1
- package/orm/tests/data-types.test.js +0 -39
- package/orm/tests/database-extension.test.d.ts +0 -1
- package/orm/tests/database-extension.test.js +0 -63
- package/orm/tests/database-migration.test.d.ts +0 -1
- package/orm/tests/database-migration.test.js +0 -83
- package/orm/tests/decorators.test.d.ts +0 -1
- package/orm/tests/decorators.test.js +0 -77
- package/orm/tests/encryption.test.d.ts +0 -1
- package/orm/tests/encryption.test.js +0 -31
- package/orm/tests/query-complex.test.d.ts +0 -1
- package/orm/tests/query-complex.test.js +0 -172
- package/orm/tests/query-converter-complex.test.d.ts +0 -1
- package/orm/tests/query-converter-complex.test.js +0 -131
- package/orm/tests/query-converter.test.d.ts +0 -1
- package/orm/tests/query-converter.test.js +0 -123
- package/orm/tests/repository-advanced.test.d.ts +0 -1
- package/orm/tests/repository-advanced.test.js +0 -189
- package/orm/tests/repository-attributes.test.d.ts +0 -1
- package/orm/tests/repository-attributes.test.js +0 -83
- package/orm/tests/repository-compound-primary-key.test.d.ts +0 -2
- package/orm/tests/repository-compound-primary-key.test.js +0 -226
- package/orm/tests/repository-comprehensive.test.d.ts +0 -1
- package/orm/tests/repository-comprehensive.test.js +0 -162
- package/orm/tests/repository-coverage.test.d.ts +0 -2
- package/orm/tests/repository-coverage.test.js +0 -242
- package/orm/tests/repository-cti-complex.test.d.ts +0 -1
- package/orm/tests/repository-cti-complex.test.js +0 -151
- package/orm/tests/repository-cti-embedded.test.d.ts +0 -1
- package/orm/tests/repository-cti-embedded.test.js +0 -178
- package/orm/tests/repository-cti-extensive.test.d.ts +0 -2
- package/orm/tests/repository-cti-extensive.test.js +0 -279
- package/orm/tests/repository-cti-mapping.test.d.ts +0 -2
- package/orm/tests/repository-cti-mapping.test.js +0 -108
- package/orm/tests/repository-cti-search.test.d.ts +0 -1
- package/orm/tests/repository-cti-search.test.js +0 -141
- package/orm/tests/repository-cti-soft-delete.test.d.ts +0 -2
- package/orm/tests/repository-cti-soft-delete.test.js +0 -103
- package/orm/tests/repository-cti-transactions.test.d.ts +0 -1
- package/orm/tests/repository-cti-transactions.test.js +0 -112
- package/orm/tests/repository-cti-upsert-many.test.d.ts +0 -2
- package/orm/tests/repository-cti-upsert-many.test.js +0 -115
- package/orm/tests/repository-cti.test.d.ts +0 -2
- package/orm/tests/repository-cti.test.js +0 -390
- package/orm/tests/repository-edge-cases.test.d.ts +0 -1
- package/orm/tests/repository-edge-cases.test.js +0 -178
- package/orm/tests/repository-expiration.test.d.ts +0 -2
- package/orm/tests/repository-expiration.test.js +0 -140
- package/orm/tests/repository-extra-coverage.test.d.ts +0 -2
- package/orm/tests/repository-extra-coverage.test.js +0 -402
- package/orm/tests/repository-mapping.test.d.ts +0 -2
- package/orm/tests/repository-mapping.test.js +0 -65
- package/orm/tests/repository-regression.test.d.ts +0 -1
- package/orm/tests/repository-regression.test.js +0 -288
- package/orm/tests/repository-search-coverage.test.d.ts +0 -1
- package/orm/tests/repository-search-coverage.test.js +0 -107
- package/orm/tests/repository-search.test.d.ts +0 -1
- package/orm/tests/repository-search.test.js +0 -105
- package/orm/tests/repository-soft-delete.test.d.ts +0 -1
- package/orm/tests/repository-soft-delete.test.js +0 -118
- package/orm/tests/repository-transactions-nested.test.d.ts +0 -1
- package/orm/tests/repository-transactions-nested.test.js +0 -178
- package/orm/tests/repository-types.test.d.ts +0 -1
- package/orm/tests/repository-types.test.js +0 -184
- package/orm/tests/repository-undelete.test.d.ts +0 -2
- package/orm/tests/repository-undelete.test.js +0 -201
- package/orm/tests/schema-converter.test.d.ts +0 -1
- package/orm/tests/schema-converter.test.js +0 -82
- package/orm/tests/schema-generation.test.d.ts +0 -2
- package/orm/tests/schema-generation.test.js +0 -174
- package/orm/tests/sql-helpers.test.d.ts +0 -1
- package/orm/tests/sql-helpers.test.js +0 -67
- package/orm/tests/transaction-safety.test.d.ts +0 -1
- package/orm/tests/transaction-safety.test.js +0 -81
- package/orm/tests/transactional.test.d.ts +0 -1
- package/orm/tests/transactional.test.js +0 -215
- package/orm/tests/utils.test.d.ts +0 -1
- package/orm/tests/utils.test.js +0 -70
- package/pdf/tests/utils.test.d.ts +0 -1
- package/pdf/tests/utils.test.js +0 -187
- package/process/tests/spawn.test.d.ts +0 -1
- package/process/tests/spawn.test.js +0 -182
- package/rate-limit/tests/postgres-rate-limiter.test.d.ts +0 -1
- package/rate-limit/tests/postgres-rate-limiter.test.js +0 -84
- package/renderer/tests/renderer.test.d.ts +0 -1
- package/renderer/tests/renderer.test.js +0 -88
- package/rpc/tests/rpc.integration.test.d.ts +0 -1
- package/rpc/tests/rpc.integration.test.js +0 -615
- package/signals/implementation/errors.d.ts +0 -2
- package/signals/implementation/graph.js +0 -312
- package/signals/implementation/writable-signal.d.ts +0 -48
- package/signals/implementation/writable-signal.js +0 -32
- package/task-queue/tests/coverage-branch.test.d.ts +0 -1
- package/task-queue/tests/coverage-branch.test.js +0 -395
- package/task-queue/tests/coverage-enhancement.test.d.ts +0 -1
- package/task-queue/tests/coverage-enhancement.test.js +0 -150
- package/task-queue/tests/dag.test.d.ts +0 -1
- package/task-queue/tests/dag.test.js +0 -188
- package/task-queue/tests/dependencies.test.d.ts +0 -1
- package/task-queue/tests/dependencies.test.js +0 -296
- package/task-queue/tests/enqueue-batch.test.d.ts +0 -1
- package/task-queue/tests/enqueue-batch.test.js +0 -125
- package/task-queue/tests/enqueue-item.test.d.ts +0 -1
- package/task-queue/tests/enqueue-item.test.js +0 -12
- package/task-queue/tests/fan-out-spawning.test.d.ts +0 -1
- package/task-queue/tests/fan-out-spawning.test.js +0 -94
- package/task-queue/tests/idempotent-replacement.test.d.ts +0 -1
- package/task-queue/tests/idempotent-replacement.test.js +0 -114
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +0 -1
- package/task-queue/tests/missing-idempotent-tasks.test.js +0 -39
- package/task-queue/tests/optimization-edge-cases.test.d.ts +0 -1
- package/task-queue/tests/optimization-edge-cases.test.js +0 -124
- package/task-queue/tests/queue-generic.test.d.ts +0 -1
- package/task-queue/tests/queue-generic.test.js +0 -8
- package/task-queue/tests/queue.test.d.ts +0 -1
- package/task-queue/tests/queue.test.js +0 -756
- package/task-queue/tests/shutdown.test.d.ts +0 -1
- package/task-queue/tests/shutdown.test.js +0 -41
- package/task-queue/tests/task-context.test.d.ts +0 -1
- package/task-queue/tests/task-context.test.js +0 -7
- package/task-queue/tests/task-union.test.d.ts +0 -1
- package/task-queue/tests/task-union.test.js +0 -18
- package/task-queue/tests/transactions.test.d.ts +0 -1
- package/task-queue/tests/transactions.test.js +0 -47
- package/task-queue/tests/typing.test.d.ts +0 -1
- package/task-queue/tests/typing.test.js +0 -9
- package/task-queue/tests/worker.test.d.ts +0 -1
- package/task-queue/tests/worker.test.js +0 -258
- package/task-queue/tests/zombie-parent.test.d.ts +0 -1
- package/task-queue/tests/zombie-parent.test.js +0 -45
- package/task-queue/tests/zombie-recovery.test.d.ts +0 -1
- package/task-queue/tests/zombie-recovery.test.js +0 -51
- package/utils/tests/backoff.test.d.ts +0 -1
- package/utils/tests/backoff.test.js +0 -41
- package/utils/tests/retry-with-backoff.test.d.ts +0 -1
- package/utils/tests/retry-with-backoff.test.js +0 -49
|
@@ -1,756 +0,0 @@
|
|
|
1
|
-
import { eq, or } from 'drizzle-orm';
|
|
2
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { CancellationToken } from '../../cancellation/index.js';
|
|
4
|
-
import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
|
|
5
|
-
import { Database } from '../../orm/server/index.js';
|
|
6
|
-
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
7
|
-
import { task as taskTable } from '../../task-queue/postgres/schemas.js';
|
|
8
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
9
|
-
import { currentTimestamp } from '../../utils/date-time.js';
|
|
10
|
-
import { Timer } from '../../utils/timer.js';
|
|
11
|
-
import { timeout } from '../../utils/timing.js';
|
|
12
|
-
import { isDefined } from '../../utils/type-guards.js';
|
|
13
|
-
describe('Queue Integration Tests', () => {
|
|
14
|
-
let injector;
|
|
15
|
-
let queue;
|
|
16
|
-
// Helper to verify state in DB
|
|
17
|
-
async function assertTaskStatus(id, state, message) {
|
|
18
|
-
const task = await queue.getTask(id);
|
|
19
|
-
expect(task?.status, message).toBe(state);
|
|
20
|
-
}
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
23
|
-
});
|
|
24
|
-
beforeEach(async () => {
|
|
25
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
26
|
-
const queueName = `test-queue-${crypto.randomUUID()}`;
|
|
27
|
-
queue = queueProvider.get(queueName, {
|
|
28
|
-
visibilityTimeout: 5000,
|
|
29
|
-
retryDelayMinimum: 100, // Fast retries for testing
|
|
30
|
-
retryDelayGrowth: 1, // Linear/No growth for predictable tests
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
afterEach(async () => {
|
|
34
|
-
// Drain the queue to prevent state leakage between tests
|
|
35
|
-
await queue.clear();
|
|
36
|
-
});
|
|
37
|
-
afterAll(async () => {
|
|
38
|
-
try {
|
|
39
|
-
if (isDefined(queue)) {
|
|
40
|
-
await queue.clear();
|
|
41
|
-
}
|
|
42
|
-
await injector.dispose();
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
// Ignore known double-dispose issue from MessageBus interaction
|
|
46
|
-
if (error instanceof Error && error.message === 'MessageBus is disposed.') {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
it('Basic FIFO Flow', async () => {
|
|
53
|
-
const t1 = await queue.enqueue('test', { value: 'first' });
|
|
54
|
-
const t2 = await queue.enqueue('test', { value: 'second' });
|
|
55
|
-
const d1 = await queue.dequeue();
|
|
56
|
-
expect(d1?.id).toBe(t1.id);
|
|
57
|
-
expect((d1?.data)['value']).toBe('first');
|
|
58
|
-
await queue.complete(d1, { result: { success: true } });
|
|
59
|
-
await assertTaskStatus(t1.id, TaskStatus.Completed, 'Task 1 completed');
|
|
60
|
-
const d2 = await queue.dequeue();
|
|
61
|
-
expect(d2?.id).toBe(t2.id);
|
|
62
|
-
await queue.complete(d2, { result: { success: true } });
|
|
63
|
-
});
|
|
64
|
-
it('Priorities', async () => {
|
|
65
|
-
// Priority 1000 (default)
|
|
66
|
-
const low = await queue.enqueue('test', { value: 'low' });
|
|
67
|
-
// Priority 1
|
|
68
|
-
const high = await queue.enqueue('test', { value: 'high' }, { priority: 1 });
|
|
69
|
-
const first = await queue.dequeue();
|
|
70
|
-
expect(first?.id).toBe(high.id);
|
|
71
|
-
await queue.complete(first);
|
|
72
|
-
const second = await queue.dequeue();
|
|
73
|
-
expect(second?.id).toBe(low.id);
|
|
74
|
-
await queue.complete(second);
|
|
75
|
-
});
|
|
76
|
-
it('Deduplication (Idempotency Keys)', async () => {
|
|
77
|
-
const key = `unique-${crypto.randomUUID()}`;
|
|
78
|
-
// 1. Initial Insert
|
|
79
|
-
const t1 = await queue.enqueue('test', { value: 'original' }, { idempotencyKey: key });
|
|
80
|
-
// 2. Default Strategy (replace: false): Should return existing task, ignore new data
|
|
81
|
-
const t2 = await queue.enqueue('test', { value: 'ignored' }, { idempotencyKey: key });
|
|
82
|
-
expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
|
|
83
|
-
const check1 = await queue.getTask(t1.id);
|
|
84
|
-
expect((check1?.data)['value']).toBe('original');
|
|
85
|
-
// 3. Replace Strategy: Should replace existing task with new data. ID stays the same to avoid foreign key violations.
|
|
86
|
-
const t3 = await queue.enqueueMany([{ type: 'test', data: { value: 'updated' }, idempotencyKey: key }], { replace: true, returnTasks: true });
|
|
87
|
-
expect(t3[0].id, 'Same ID if replaced').toBe(t1.id);
|
|
88
|
-
// New task should have new data
|
|
89
|
-
const checkNew = await queue.getTask(t3[0].id);
|
|
90
|
-
expect((checkNew?.data)['value']).toBe('updated');
|
|
91
|
-
expect(checkNew?.tries).toBe(0);
|
|
92
|
-
});
|
|
93
|
-
it('Retries and Failures', async () => {
|
|
94
|
-
const task = await queue.enqueue('test', { value: 'fail-me' });
|
|
95
|
-
// Try 1
|
|
96
|
-
const attempt1 = await queue.dequeue();
|
|
97
|
-
expect(attempt1?.id).toBe(task.id);
|
|
98
|
-
await queue.fail(attempt1, { message: 'oops' });
|
|
99
|
-
// Verify Retrying status
|
|
100
|
-
await assertTaskStatus(task.id, TaskStatus.Retrying, 'Task enters Retrying status after failure');
|
|
101
|
-
// Force reschedule to now to bypass retryDelay
|
|
102
|
-
await queue.reschedule(task.id, currentTimestamp());
|
|
103
|
-
// Try 2
|
|
104
|
-
const attempt2 = await queue.dequeue();
|
|
105
|
-
expect(attempt2?.id).toBe(task.id);
|
|
106
|
-
expect(attempt2?.tries).toBe(2);
|
|
107
|
-
// Fail fatally
|
|
108
|
-
await queue.fail(attempt2, { message: 'fatal error' }, { fatal: true });
|
|
109
|
-
await assertTaskStatus(task.id, TaskStatus.Dead, 'Task is Dead after fatal error');
|
|
110
|
-
});
|
|
111
|
-
it('Hierarchy (Parent/Child)', async () => {
|
|
112
|
-
// A. Create Parent
|
|
113
|
-
const p = await queue.enqueue('test', { value: 'parent-manual' });
|
|
114
|
-
// B. Dequeue Parent
|
|
115
|
-
const pTask = await queue.dequeue();
|
|
116
|
-
expect(pTask?.id).toBe(p.id);
|
|
117
|
-
// C. Parent spawns child
|
|
118
|
-
const child = await queue.enqueue('test', { value: 'child-manual' }, { parentId: p.id });
|
|
119
|
-
// D. "Finish" Parent execution.
|
|
120
|
-
await queue.complete(pTask);
|
|
121
|
-
// await assertTaskStatus(p.id, TaskStatus.Waiting, 'Parent entered WAITING state'); // Depends on implementation details of auto-waiting
|
|
122
|
-
});
|
|
123
|
-
it('Batching', async () => {
|
|
124
|
-
const batch = queue.batch();
|
|
125
|
-
for (let i = 0; i < 5; i++) {
|
|
126
|
-
batch.add('test', { value: `batch-${i}` });
|
|
127
|
-
}
|
|
128
|
-
const tasks = await batch.enqueue({ returnTasks: true });
|
|
129
|
-
expect(tasks.length).toBe(5);
|
|
130
|
-
const dequeuedBatch = await queue.dequeueMany(5);
|
|
131
|
-
expect(dequeuedBatch.length).toBe(5);
|
|
132
|
-
await queue.completeMany(dequeuedBatch);
|
|
133
|
-
const leftover = await queue.dequeue();
|
|
134
|
-
expect(leftover).toBeUndefined();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
138
|
-
let injector;
|
|
139
|
-
let queue;
|
|
140
|
-
let database;
|
|
141
|
-
beforeAll(async () => {
|
|
142
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
143
|
-
database = injector.resolve(Database);
|
|
144
|
-
});
|
|
145
|
-
beforeEach(() => {
|
|
146
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
147
|
-
const queueName = `pg-test-queue-${crypto.randomUUID()}`;
|
|
148
|
-
queue = queueProvider.get(queueName, {
|
|
149
|
-
visibilityTimeout: 50, // Short timeout for testing
|
|
150
|
-
retryDelayMinimum: 50,
|
|
151
|
-
retryDelayGrowth: 1,
|
|
152
|
-
circuitBreakerThreshold: 2,
|
|
153
|
-
circuitBreakerResetTimeout: 50,
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
afterEach(async () => {
|
|
157
|
-
await queue.clear();
|
|
158
|
-
});
|
|
159
|
-
afterAll(async () => {
|
|
160
|
-
await injector?.dispose();
|
|
161
|
-
});
|
|
162
|
-
describe('Basic Lifecycle', () => {
|
|
163
|
-
it('should enqueue and dequeue a task', async () => {
|
|
164
|
-
await queue.enqueue('foo', { foo: 'bar' });
|
|
165
|
-
const task = await queue.dequeue();
|
|
166
|
-
expect(task).toBeDefined();
|
|
167
|
-
expect(task?.data).toEqual({ foo: 'bar' });
|
|
168
|
-
expect(task?.status).toBe(TaskStatus.Running);
|
|
169
|
-
expect(task?.tries).toBe(1);
|
|
170
|
-
});
|
|
171
|
-
it('should complete a task successfully', async () => {
|
|
172
|
-
const task = await queue.enqueue('foo', { foo: 'bar' });
|
|
173
|
-
const dequeued = await queue.dequeue();
|
|
174
|
-
await queue.complete(dequeued, { result: { result: true } });
|
|
175
|
-
const updated = await queue.getTask(task.id);
|
|
176
|
-
expect(updated?.status).toBe(TaskStatus.Completed);
|
|
177
|
-
expect(updated?.result).toEqual({ result: true });
|
|
178
|
-
expect(updated.completeTimestamp > 0).toBe(true);
|
|
179
|
-
});
|
|
180
|
-
it('should fail a task and increment tries', async () => {
|
|
181
|
-
const task = await queue.enqueue('foo', { foo: 'bar' });
|
|
182
|
-
const dequeued = await queue.dequeue();
|
|
183
|
-
await queue.fail(dequeued, new Error('temp failure'));
|
|
184
|
-
const updated = await queue.getTask(task.id);
|
|
185
|
-
expect(updated?.status).toBe(TaskStatus.Retrying);
|
|
186
|
-
expect(updated?.tries).toBe(1);
|
|
187
|
-
expect(updated?.errors).toHaveLength(1);
|
|
188
|
-
});
|
|
189
|
-
it('should NOT clear startTimestamp when transitioning to terminal or retry states', async () => {
|
|
190
|
-
// Re-create queue with high circuit breaker threshold for this test
|
|
191
|
-
const q = injector.resolve(TaskQueueProvider).get(`start-timestamp-${crypto.randomUUID()}`, {
|
|
192
|
-
circuitBreakerThreshold: 10,
|
|
193
|
-
});
|
|
194
|
-
const task = await q.enqueue('foo', { foo: 'bar' });
|
|
195
|
-
// 1. Dequeue to start it (sets startTimestamp)
|
|
196
|
-
const dequeued = await q.dequeue();
|
|
197
|
-
expect(dequeued?.startTimestamp).not.toBeNull();
|
|
198
|
-
const startTimestamp = dequeued.startTimestamp;
|
|
199
|
-
// 2. Fail it (transitions to Retrying)
|
|
200
|
-
await q.fail(dequeued, new Error('temp failure'));
|
|
201
|
-
const retryingTask = await q.getTask(task.id);
|
|
202
|
-
expect(retryingTask?.status).toBe(TaskStatus.Retrying);
|
|
203
|
-
expect(retryingTask?.startTimestamp).toBe(startTimestamp);
|
|
204
|
-
// 3. Dequeue again (updates startTimestamp)
|
|
205
|
-
await q.reschedule(task.id, currentTimestamp());
|
|
206
|
-
const dequeued2 = await q.dequeue();
|
|
207
|
-
expect(dequeued2?.startTimestamp).not.toBe(startTimestamp);
|
|
208
|
-
const startTimestamp2 = dequeued2.startTimestamp;
|
|
209
|
-
// 4. Fail fatally (transitions to Dead)
|
|
210
|
-
await q.fail(dequeued2, new Error('fatal failure'), { fatal: true });
|
|
211
|
-
const deadTask = await q.getTask(task.id);
|
|
212
|
-
expect(deadTask?.status).toBe(TaskStatus.Dead);
|
|
213
|
-
expect(deadTask?.startTimestamp).toBe(startTimestamp2);
|
|
214
|
-
// 5. Success (transitions to Completed)
|
|
215
|
-
const task2 = await q.enqueue('foo', { foo: 'bar2' });
|
|
216
|
-
const dequeued3 = await q.dequeue();
|
|
217
|
-
expect(dequeued3).toBeDefined();
|
|
218
|
-
const startTimestamp3 = dequeued3.startTimestamp;
|
|
219
|
-
await q.complete(dequeued3);
|
|
220
|
-
const completedTask = await q.getTask(task2.id);
|
|
221
|
-
expect(completedTask?.status).toBe(TaskStatus.Completed);
|
|
222
|
-
expect(completedTask?.startTimestamp).toBe(startTimestamp3);
|
|
223
|
-
await q.clear();
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
describe('Hierarchy and Cross-Namespace', () => {
|
|
227
|
-
it('should correctly increment parent unresolved dependencies when a child is spawned in a different namespace', async () => {
|
|
228
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
229
|
-
const nameA = `QueueA-${crypto.randomUUID()}`;
|
|
230
|
-
const nameB = `QueueB-${crypto.randomUUID()}`;
|
|
231
|
-
const queueA = queueProvider.get(nameA);
|
|
232
|
-
const queueB = queueProvider.get(nameB);
|
|
233
|
-
const parent = await queueA.enqueue('test', { value: 'parent' });
|
|
234
|
-
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
235
|
-
await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id, parentRequires: true });
|
|
236
|
-
const updatedParent = await queueA.getTask(parent.id);
|
|
237
|
-
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
238
|
-
await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
|
|
239
|
-
await queueB.clear();
|
|
240
|
-
await queueA.clear();
|
|
241
|
-
});
|
|
242
|
-
it('should recursively cancel the entire task tree spanning multiple namespaces', async () => {
|
|
243
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
244
|
-
const nameA = `NamespaceA-${crypto.randomUUID()}`;
|
|
245
|
-
const nameB = `NamespaceB-${crypto.randomUUID()}`;
|
|
246
|
-
const queueA = queueProvider.get(nameA);
|
|
247
|
-
const queueB = queueProvider.get(nameB);
|
|
248
|
-
const parent = await queueA.enqueue('test', { value: 'parent' });
|
|
249
|
-
const child = await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id });
|
|
250
|
-
const grandchild = await queueA.enqueue('test', { value: 'grandchild' }, { parentId: child.id });
|
|
251
|
-
await queueA.cancel(parent.id);
|
|
252
|
-
await queueB.waitForTasks([child.id], { statuses: [TaskStatus.Cancelled] });
|
|
253
|
-
await queueA.waitForTasks([grandchild.id], { statuses: [TaskStatus.Cancelled] });
|
|
254
|
-
const updatedChild = await queueB.getTask(child.id);
|
|
255
|
-
const updatedGrandchild = await queueA.getTask(grandchild.id);
|
|
256
|
-
expect(updatedChild?.status).toBe(TaskStatus.Cancelled);
|
|
257
|
-
expect(updatedGrandchild?.status).toBe(TaskStatus.Cancelled);
|
|
258
|
-
await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
|
|
259
|
-
await queueB.clear();
|
|
260
|
-
await queueA.clear();
|
|
261
|
-
});
|
|
262
|
-
it('should successfully cancel tasks from other namespaces (Bug 4)', async () => {
|
|
263
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
264
|
-
const nameA = `QueueA-${crypto.randomUUID()}`;
|
|
265
|
-
const nameB = `QueueB-${crypto.randomUUID()}`;
|
|
266
|
-
const queueA = queueProvider.get(nameA);
|
|
267
|
-
const queueB = queueProvider.get(nameB);
|
|
268
|
-
// 1. Enqueue task in queueB
|
|
269
|
-
const taskB = await queueB.enqueue('test', {});
|
|
270
|
-
expect(taskB.status).toBe(TaskStatus.Pending);
|
|
271
|
-
// 2. Cancel task in queueA (using taskB.id which is from other namespace)
|
|
272
|
-
await queueA.cancel(taskB.id);
|
|
273
|
-
// 3. Verify taskB IS cancelled (because ID is global)
|
|
274
|
-
const updatedB = await queueB.getTask(taskB.id);
|
|
275
|
-
expect(updatedB?.status).toBe(TaskStatus.Cancelled);
|
|
276
|
-
await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
|
|
277
|
-
await queueB.clear();
|
|
278
|
-
await queueA.clear();
|
|
279
|
-
});
|
|
280
|
-
it('should complete parent task if idempotent child is already completed (Bug 6)', async () => {
|
|
281
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
282
|
-
// 1. Enqueue and complete child task with idempotency key
|
|
283
|
-
const idempotencyKey = 'child-idempotency-key';
|
|
284
|
-
const child = await q.enqueue('test', {}, { idempotencyKey });
|
|
285
|
-
const dequeuedChild = await q.dequeue();
|
|
286
|
-
await q.complete(dequeuedChild);
|
|
287
|
-
const finishedChild = await q.getTask(child.id);
|
|
288
|
-
expect(finishedChild?.status).toBe(TaskStatus.Completed);
|
|
289
|
-
// 2. Enqueue parent task that spawns the same child (via idempotency key)
|
|
290
|
-
const parent = await q.enqueue('test', {});
|
|
291
|
-
await q.enqueue('test', {}, {
|
|
292
|
-
idempotencyKey,
|
|
293
|
-
parentId: parent.id,
|
|
294
|
-
parentRequires: true,
|
|
295
|
-
});
|
|
296
|
-
// Dequeue and complete parent
|
|
297
|
-
const dequeuedParent = await q.dequeue();
|
|
298
|
-
await q.complete(dequeuedParent);
|
|
299
|
-
const updatedParent = await q.getTask(parent.id);
|
|
300
|
-
expect(updatedParent?.status).toBe(TaskStatus.Completed);
|
|
301
|
-
await q.clear();
|
|
302
|
-
});
|
|
303
|
-
it('should increment unresolvedCompleteDependencies for children with parentRequires: true (Bug 6-2)', async () => {
|
|
304
|
-
const parent = await queue.enqueue('parent', {});
|
|
305
|
-
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
306
|
-
await queue.enqueue('child', {}, { parentId: parent.id, parentRequires: true });
|
|
307
|
-
const updatedParent = await queue.getTask(parent.id);
|
|
308
|
-
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
describe('Priority and Scheduling', () => {
|
|
312
|
-
it('should dequeue tasks in priority order (lower number first)', async () => {
|
|
313
|
-
await queue.enqueue('foo', { foo: 'low' }, { priority: 2000 });
|
|
314
|
-
await queue.enqueue('foo', { foo: 'high' }, { priority: 10 });
|
|
315
|
-
await queue.enqueue('foo', { foo: 'mid' }, { priority: 1000 });
|
|
316
|
-
const t1 = await queue.dequeue();
|
|
317
|
-
const t2 = await queue.dequeue();
|
|
318
|
-
const t3 = await queue.dequeue();
|
|
319
|
-
expect((t1?.data)['foo']).toBe('high');
|
|
320
|
-
expect((t2?.data)['foo']).toBe('mid');
|
|
321
|
-
expect((t3?.data)['foo']).toBe('low');
|
|
322
|
-
});
|
|
323
|
-
it('should not dequeue a task scheduled in the future', async () => {
|
|
324
|
-
const future = currentTimestamp() + 100;
|
|
325
|
-
await queue.enqueue('foo', { foo: 'future' }, { scheduleTimestamp: future });
|
|
326
|
-
const task = await queue.dequeue();
|
|
327
|
-
expect(task).toBeUndefined();
|
|
328
|
-
await timeout(150);
|
|
329
|
-
const taskLater = await queue.dequeue();
|
|
330
|
-
expect(taskLater).toBeDefined();
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
describe('Concurrency Control', () => {
|
|
334
|
-
it('should refund the rate limiter tokens when dequeueMany retrieves fewer tasks than requested', async () => {
|
|
335
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, {
|
|
336
|
-
rateLimit: 5,
|
|
337
|
-
rateInterval: 60000,
|
|
338
|
-
});
|
|
339
|
-
await q.enqueue('test', { value: '1' });
|
|
340
|
-
const tasks1 = await q.dequeueMany(5); // Consumes 5, gets 1, refunds 4.
|
|
341
|
-
expect(tasks1).toHaveLength(1);
|
|
342
|
-
await q.enqueue('test', { value: '2' });
|
|
343
|
-
const tasks2 = await q.dequeueMany(4); // Should be allowed because of refund.
|
|
344
|
-
expect(tasks2).toHaveLength(1);
|
|
345
|
-
await q.clear();
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
describe('Circuit Breaker', () => {
|
|
349
|
-
it('should trip the breaker after threshold failures', async () => {
|
|
350
|
-
// Config: circuitBreakerThreshold: 2 (set in beforeEach)
|
|
351
|
-
await queue.enqueue('foo', { foo: '1' });
|
|
352
|
-
await queue.enqueue('foo', { foo: '2' });
|
|
353
|
-
await queue.enqueue('foo', { foo: '3' });
|
|
354
|
-
await queue.fail((await queue.dequeue()), 'err');
|
|
355
|
-
await queue.fail((await queue.dequeue()), 'err');
|
|
356
|
-
// Breaker should be Open
|
|
357
|
-
const t3Attempt = await queue.dequeue();
|
|
358
|
-
expect(t3Attempt).toBeUndefined();
|
|
359
|
-
});
|
|
360
|
-
it('should only allow a single active task in Half-Open state', async () => {
|
|
361
|
-
await queue.enqueueMany([
|
|
362
|
-
{ type: 'foo', data: { foo: '1' } },
|
|
363
|
-
{ type: 'foo', data: { foo: '2' } },
|
|
364
|
-
]);
|
|
365
|
-
await queue.fail((await queue.dequeue()), 'err');
|
|
366
|
-
await queue.fail((await queue.dequeue()), 'err');
|
|
367
|
-
// Breaker is Open. Wait for reset timeout (50ms)
|
|
368
|
-
await timeout(75);
|
|
369
|
-
const probe = await queue.dequeue();
|
|
370
|
-
expect(probe).toBeDefined();
|
|
371
|
-
const secondAttempt = await queue.dequeue();
|
|
372
|
-
expect(secondAttempt).toBeUndefined(); // Only 1 task allowed to RUN in Half-Open
|
|
373
|
-
});
|
|
374
|
-
it('should not stall forever when circuit breaker is Half-Open and no tasks are found', async () => {
|
|
375
|
-
// 1. Trip the breaker
|
|
376
|
-
await queue.enqueue('foo', { foo: '1' });
|
|
377
|
-
await queue.enqueue('foo', { foo: '2' });
|
|
378
|
-
await queue.fail((await queue.dequeue()), 'err', { fatal: true });
|
|
379
|
-
await queue.fail((await queue.dequeue()), 'err', { fatal: true });
|
|
380
|
-
const cb = injector.resolve(CircuitBreaker, queue.namespace);
|
|
381
|
-
let status = await cb.check();
|
|
382
|
-
expect(status.state).toBe(CircuitBreakerState.Open);
|
|
383
|
-
expect(status.allowed).toBe(false);
|
|
384
|
-
// 2. Wait for timeout (50ms)
|
|
385
|
-
await timeout(75);
|
|
386
|
-
// 3. First dequeue - should be the probe, but find nothing (we enqueued and failed the only task)
|
|
387
|
-
const tasks1 = await queue.dequeueMany(1);
|
|
388
|
-
expect(tasks1).toHaveLength(0);
|
|
389
|
-
status = await cb.check();
|
|
390
|
-
expect(status.state).toBe(CircuitBreakerState.HalfOpen);
|
|
391
|
-
// 4. Second dequeue - should still be allowed (but find nothing)
|
|
392
|
-
const tasks2 = await queue.dequeueMany(1);
|
|
393
|
-
expect(tasks2).toHaveLength(0);
|
|
394
|
-
// 5. Add a task
|
|
395
|
-
await queue.enqueue('foo', { foo: 'new' });
|
|
396
|
-
// 6. Third dequeue - should find the task now!
|
|
397
|
-
const tasks3 = await queue.dequeueMany(1);
|
|
398
|
-
expect(tasks3).toHaveLength(1);
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
describe('Timeouts and Maintenance (Pruning)', () => {
|
|
402
|
-
it('should recover "Zombie" tasks (crashed workers) and preserve startTimestamp', async () => {
|
|
403
|
-
const task = await queue.enqueue('foo', { foo: 'zombie' });
|
|
404
|
-
const dequeued = await queue.dequeue(); // Task is now Running with a token
|
|
405
|
-
expect(dequeued?.startTimestamp).not.toBeNull();
|
|
406
|
-
const startTimestamp = dequeued.startTimestamp;
|
|
407
|
-
// processTimeout is 50ms. Wait for it to expire.
|
|
408
|
-
await timeout(100);
|
|
409
|
-
await queue.maintenance();
|
|
410
|
-
const recovered = await queue.getTask(task.id);
|
|
411
|
-
expect(recovered?.status).toBe(TaskStatus.Retrying);
|
|
412
|
-
expect(recovered?.tries).toBe(1);
|
|
413
|
-
expect(recovered?.token).toBeNull();
|
|
414
|
-
expect(recovered?.startTimestamp).toBe(startTimestamp);
|
|
415
|
-
});
|
|
416
|
-
it('should fail tasks that exceed Hard Execution Timeout via prune', async () => {
|
|
417
|
-
// Re-configure queue with very short execution timeout
|
|
418
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
419
|
-
const shortQueue = queueProvider.get(`prune-test-${crypto.randomUUID()}`, { maxExecutionTime: 50 });
|
|
420
|
-
const task = await shortQueue.enqueue('foo', { foo: 'long-running' });
|
|
421
|
-
await shortQueue.dequeue();
|
|
422
|
-
await timeout(75);
|
|
423
|
-
await shortQueue.maintenance();
|
|
424
|
-
const updated = await shortQueue.getTask(task.id);
|
|
425
|
-
expect(updated?.status).toBe(TaskStatus.TimedOut);
|
|
426
|
-
expect(updated?.errors[0]?.code).toBe('MaxTimeExceeded');
|
|
427
|
-
await shortQueue.clear();
|
|
428
|
-
});
|
|
429
|
-
it('should touch a task to extend token', async () => {
|
|
430
|
-
const task = await queue.enqueue('foo', { foo: 'work' });
|
|
431
|
-
const dequeued = await queue.dequeue();
|
|
432
|
-
const initialLock = dequeued.visibilityDeadline;
|
|
433
|
-
await timeout(20);
|
|
434
|
-
const touched = await queue.touch(dequeued);
|
|
435
|
-
expect(touched.visibilityDeadline > initialLock).toBe(true);
|
|
436
|
-
});
|
|
437
|
-
it('should prevent touching if token is lost (stolen by another worker)', async () => {
|
|
438
|
-
await queue.enqueue('foo', { foo: 'work' });
|
|
439
|
-
const dequeued = await queue.dequeue();
|
|
440
|
-
expect(dequeued).toBeDefined();
|
|
441
|
-
// processTimeout is 50ms. Wait for it to expire.
|
|
442
|
-
await timeout(100);
|
|
443
|
-
await queue.maintenance();
|
|
444
|
-
await queue.dequeue(); // Stolen by another worker (tries=2)
|
|
445
|
-
// Original worker tries to touch
|
|
446
|
-
const touchResult = await queue.touch(dequeued);
|
|
447
|
-
expect(touchResult).toBeUndefined();
|
|
448
|
-
});
|
|
449
|
-
it('should correctly identify and transition zombie tasks to Orphaned after max retries', async () => {
|
|
450
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, { maxTries: 1 });
|
|
451
|
-
const task = await q.enqueue('test', { value: 'zombie' });
|
|
452
|
-
// Dequeue to start it
|
|
453
|
-
await q.dequeue();
|
|
454
|
-
// Manually make it a zombie
|
|
455
|
-
await database
|
|
456
|
-
.update(taskTable)
|
|
457
|
-
.set({ visibilityDeadline: currentTimestamp() - 1000 })
|
|
458
|
-
.where(eq(taskTable.id, task.id));
|
|
459
|
-
await q.maintenance();
|
|
460
|
-
const updated = await q.getTask(task.id);
|
|
461
|
-
expect(updated?.status).toBe(TaskStatus.Orphaned);
|
|
462
|
-
await q.clear();
|
|
463
|
-
});
|
|
464
|
-
it('should age priority correctly', async () => {
|
|
465
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, {
|
|
466
|
-
priorityAgingInterval: 0,
|
|
467
|
-
priorityAgingStep: 10,
|
|
468
|
-
});
|
|
469
|
-
const task = await q.enqueue('test', { value: 'aging' }, { priority: 100 });
|
|
470
|
-
// Manually backdate priorityAgeTimestamp
|
|
471
|
-
await database
|
|
472
|
-
.update(taskTable)
|
|
473
|
-
.set({ priorityAgeTimestamp: currentTimestamp() - 1000 })
|
|
474
|
-
.where(eq(taskTable.id, task.id));
|
|
475
|
-
await q.maintenance();
|
|
476
|
-
const updated = await q.getTask(task.id);
|
|
477
|
-
expect(updated?.priority).toBe(90);
|
|
478
|
-
await q.clear();
|
|
479
|
-
});
|
|
480
|
-
it('clear() should NOT throw foreign key violations', async () => {
|
|
481
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
482
|
-
const otherQueue = queueProvider.get('other-namespace', {});
|
|
483
|
-
const parent = await queue.enqueue('parent', {});
|
|
484
|
-
await otherQueue.enqueue('child', {}, { parentId: parent.id });
|
|
485
|
-
// Clearing the queue containing the parent should NOT throw even if other-namespace has a child pointing to it.
|
|
486
|
-
await otherQueue.clear();
|
|
487
|
-
await expect(queue.clear()).resolves.toBeUndefined();
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
describe('Batch Operations', () => {
|
|
491
|
-
it('should complete many tasks efficiently', async () => {
|
|
492
|
-
const tasks = await queue.enqueueMany([
|
|
493
|
-
{ type: 'foo', data: { foo: '1' } },
|
|
494
|
-
{ type: 'foo', data: { foo: '2' } },
|
|
495
|
-
], { returnTasks: true });
|
|
496
|
-
const d1 = await queue.dequeue();
|
|
497
|
-
const d2 = await queue.dequeue();
|
|
498
|
-
await queue.completeMany([d1, d2]);
|
|
499
|
-
const t1 = await queue.getTask(tasks[0].id);
|
|
500
|
-
const t2 = await queue.getTask(tasks[1].id);
|
|
501
|
-
expect(t1?.status).toBe(TaskStatus.Completed);
|
|
502
|
-
expect(t2?.status).toBe(TaskStatus.Completed);
|
|
503
|
-
});
|
|
504
|
-
it('should successfully completeMany tasks that have a NULL token', async () => {
|
|
505
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
506
|
-
const t1 = await q.enqueue('test', { value: '1' });
|
|
507
|
-
const t2 = await q.enqueue('test', { value: '2' });
|
|
508
|
-
expect(t1.token).toBeNull();
|
|
509
|
-
expect(t2.token).toBeNull();
|
|
510
|
-
await q.completeMany([t1, t2]);
|
|
511
|
-
const ut1 = await q.getTask(t1.id);
|
|
512
|
-
const ut2 = await q.getTask(t2.id);
|
|
513
|
-
expect(ut1?.status).toBe(TaskStatus.Completed);
|
|
514
|
-
expect(ut2?.status).toBe(TaskStatus.Completed);
|
|
515
|
-
await q.clear();
|
|
516
|
-
});
|
|
517
|
-
it('should successfully failMany tasks that have a NULL token', async () => {
|
|
518
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, { maxTries: 0 });
|
|
519
|
-
const t1 = await q.enqueue('test', { value: '1' });
|
|
520
|
-
await q.failMany([t1], [new Error('fail')]);
|
|
521
|
-
const ut1 = await q.getTask(t1.id);
|
|
522
|
-
expect(ut1?.status).toBe(TaskStatus.Dead);
|
|
523
|
-
await q.clear();
|
|
524
|
-
});
|
|
525
|
-
it('should successfully touchMany tasks with missing or NULL states', async () => {
|
|
526
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
527
|
-
const t1 = await q.enqueue('test', { value: '1' });
|
|
528
|
-
const t2 = await q.enqueue('test', { value: '2' });
|
|
529
|
-
const d1 = await q.dequeue();
|
|
530
|
-
const d2 = await q.dequeue();
|
|
531
|
-
await q.touchMany([d1, d2], {
|
|
532
|
-
progresses: [0.5, 0.8],
|
|
533
|
-
states: [{ step: 'halfway' }, undefined],
|
|
534
|
-
});
|
|
535
|
-
const ut1 = await q.getTask(t1.id);
|
|
536
|
-
const ut2 = await q.getTask(t2.id);
|
|
537
|
-
expect(ut1?.progress).toBe(0.5);
|
|
538
|
-
expect(ut1?.state).toEqual({ step: 'halfway' });
|
|
539
|
-
expect(ut2?.progress).toBe(0.8);
|
|
540
|
-
expect(ut2?.state).toBeNull();
|
|
541
|
-
await q.clear();
|
|
542
|
-
});
|
|
543
|
-
it('should reject bulk updates (completeMany/failMany) if the provided token does not match the database', async () => {
|
|
544
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
545
|
-
const t1 = await q.enqueue('test', { value: '1' });
|
|
546
|
-
// Task is Pending, token is NULL.
|
|
547
|
-
// Try to complete with a fake token.
|
|
548
|
-
await q.completeMany([{ ...t1, token: crypto.randomUUID() }]);
|
|
549
|
-
const ut1 = await q.getTask(t1.id);
|
|
550
|
-
expect(ut1?.status).toBe(TaskStatus.Pending); // Should still be Pending
|
|
551
|
-
// Dequeue to get a real token
|
|
552
|
-
const d1 = await q.dequeue();
|
|
553
|
-
expect(d1?.token).toBeDefined();
|
|
554
|
-
// Try to complete with NULL token
|
|
555
|
-
await q.completeMany([{ ...d1, token: null }]);
|
|
556
|
-
const ut2 = await q.getTask(t1.id);
|
|
557
|
-
expect(ut2?.status).toBe(TaskStatus.Running); // Should still be Running
|
|
558
|
-
await q.clear();
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
describe('Rescheduling', () => {
|
|
562
|
-
it('should reschedule and refund tries if running', async () => {
|
|
563
|
-
const task = await queue.enqueue('foo', { foo: 'reschedule-me' });
|
|
564
|
-
const dequeued = await queue.dequeue();
|
|
565
|
-
expect(dequeued?.tries).toBe(1);
|
|
566
|
-
const inFuture = currentTimestamp() + 1000;
|
|
567
|
-
await queue.reschedule(dequeued.id, inFuture);
|
|
568
|
-
const updated = await queue.getTask(task.id);
|
|
569
|
-
expect(updated?.status).toBe(TaskStatus.Pending);
|
|
570
|
-
expect(updated?.tries).toBe(0); // Refunded
|
|
571
|
-
expect(updated?.scheduleTimestamp).toBe(inFuture);
|
|
572
|
-
});
|
|
573
|
-
it('rescheduling should NOT bypass dependency constraints (Bug 7)', async () => {
|
|
574
|
-
const dep = await queue.enqueue('dep', {});
|
|
575
|
-
const main = await queue.enqueue('main', {}, { scheduleAfter: [dep.id] });
|
|
576
|
-
expect(main.status).toBe(TaskStatus.Waiting);
|
|
577
|
-
expect(main.unresolvedScheduleDependencies).toBe(1);
|
|
578
|
-
await queue.reschedule(main.id, currentTimestamp());
|
|
579
|
-
const updatedMain = await queue.getTask(main.id);
|
|
580
|
-
expect(updatedMain?.status).toBe(TaskStatus.Waiting);
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
describe('TaskContext (Worker DX)', () => {
|
|
584
|
-
it('checkpoint() should update progress and handle token loss', async () => {
|
|
585
|
-
const task = await queue.enqueue('foo', { foo: 'progress' });
|
|
586
|
-
const dequeued = await queue.dequeue();
|
|
587
|
-
// In real scenarios TaskContext wraps the queue logic.
|
|
588
|
-
// Here we just verify touch/checkpoint effects on the DB.
|
|
589
|
-
await queue.touch(dequeued, { progress: 0.5, state: { step: 1 } });
|
|
590
|
-
const updated = await queue.getTask(task.id);
|
|
591
|
-
expect(updated?.progress).toBe(0.5);
|
|
592
|
-
expect(updated?.state).toEqual({ step: 1 });
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
describe('waitForTasks', () => {
|
|
596
|
-
it('should wait for multiple tasks to reach finalized state', async () => {
|
|
597
|
-
const t1 = await queue.enqueue('foo', { foo: 'wait-1' });
|
|
598
|
-
const t2 = await queue.enqueue('foo', { foo: 'wait-2' });
|
|
599
|
-
void (async () => {
|
|
600
|
-
await timeout(100);
|
|
601
|
-
const d1 = await queue.dequeue();
|
|
602
|
-
await queue.complete(d1);
|
|
603
|
-
await timeout(100);
|
|
604
|
-
const d2 = await queue.dequeue();
|
|
605
|
-
await queue.complete(d2);
|
|
606
|
-
})();
|
|
607
|
-
const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
|
|
608
|
-
expect(result.cancelled).toBe(false);
|
|
609
|
-
const check1 = await queue.getTask(t1.id);
|
|
610
|
-
const check2 = await queue.getTask(t2.id);
|
|
611
|
-
expect(check1?.status).toBe(TaskStatus.Completed);
|
|
612
|
-
expect(check2?.status).toBe(TaskStatus.Completed);
|
|
613
|
-
});
|
|
614
|
-
it('should throw TimeoutError on timeout', async () => {
|
|
615
|
-
const t1 = await queue.enqueue('foo', { foo: 'timeout' });
|
|
616
|
-
await expect(queue.waitForTasks([t1.id], { timeout: 100 })).rejects.toThrow('Timeout while waiting for tasks to complete');
|
|
617
|
-
});
|
|
618
|
-
it('should wait for Cancelled and Dead states', async () => {
|
|
619
|
-
const t1 = await queue.enqueue('foo', { foo: 'cancel' });
|
|
620
|
-
const t2 = await queue.enqueue('foo', { foo: 'dead' });
|
|
621
|
-
void (async () => {
|
|
622
|
-
await timeout(50);
|
|
623
|
-
const d1 = await queue.dequeue();
|
|
624
|
-
if (d1)
|
|
625
|
-
await queue.cancel(d1.id);
|
|
626
|
-
const d2 = await queue.dequeue();
|
|
627
|
-
if (d2)
|
|
628
|
-
await queue.fail(d2, new Error('fatal'), { fatal: true });
|
|
629
|
-
queue.notify();
|
|
630
|
-
})();
|
|
631
|
-
const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
|
|
632
|
-
expect(result.cancelled).toBe(false);
|
|
633
|
-
const c1 = await queue.getTask(t1.id);
|
|
634
|
-
const c2 = await queue.getTask(t2.id);
|
|
635
|
-
expect(c1?.status).toBe(TaskStatus.Cancelled);
|
|
636
|
-
expect(c2?.status).toBe(TaskStatus.Dead);
|
|
637
|
-
});
|
|
638
|
-
it('should handle cancellationSignal', async () => {
|
|
639
|
-
const t1 = await queue.enqueue('foo', { foo: 'long' });
|
|
640
|
-
const signal = new CancellationToken();
|
|
641
|
-
void timeout(100).then(() => signal.set());
|
|
642
|
-
const result = await queue.waitForTasks([t1.id], { cancellationSignal: signal, timeout: 5000 });
|
|
643
|
-
expect(result.cancelled).toBe(true);
|
|
644
|
-
});
|
|
645
|
-
it('should return immediately for non-existent tasks', async () => {
|
|
646
|
-
const result = await queue.waitForTasks([crypto.randomUUID()], { timeout: 1000 });
|
|
647
|
-
expect(result.cancelled).toBe(false);
|
|
648
|
-
});
|
|
649
|
-
it('should return immediately if all tasks are already finalized', async () => {
|
|
650
|
-
const t1 = await queue.enqueue('foo', { foo: 'immediate' });
|
|
651
|
-
const d1 = await queue.dequeue();
|
|
652
|
-
await queue.complete(d1);
|
|
653
|
-
const timer = Timer.startNew();
|
|
654
|
-
const result = await queue.waitForTasks([t1.id], { timeout: 1000 });
|
|
655
|
-
expect(result.cancelled).toBe(false);
|
|
656
|
-
expect(timer.milliseconds).toBeLessThan(100);
|
|
657
|
-
});
|
|
658
|
-
it('should handle a mix of active and archived tasks', async () => {
|
|
659
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
660
|
-
const archiveQueue = queueProvider.get(`archive-test-${crypto.randomUUID()}`, {
|
|
661
|
-
retention: 0, // Archive immediately
|
|
662
|
-
});
|
|
663
|
-
const t1 = await archiveQueue.enqueue('foo', { foo: 'archived' });
|
|
664
|
-
const d1 = await archiveQueue.dequeue();
|
|
665
|
-
await archiveQueue.complete(d1);
|
|
666
|
-
// Run maintenance to move to archive
|
|
667
|
-
await archiveQueue.maintenance();
|
|
668
|
-
const t2 = await archiveQueue.enqueue('foo', { foo: 'active' });
|
|
669
|
-
const d2 = await archiveQueue.dequeue();
|
|
670
|
-
await archiveQueue.complete(d2);
|
|
671
|
-
// t1 is archived, t2 is completed (active)
|
|
672
|
-
const result = await archiveQueue.waitForTasks([t1.id, t2.id], { timeout: 1000 });
|
|
673
|
-
expect(result.cancelled).toBe(false);
|
|
674
|
-
await archiveQueue.clear();
|
|
675
|
-
});
|
|
676
|
-
it('should return immediately for empty ids array', async () => {
|
|
677
|
-
const result = await queue.waitForTasks([], { timeout: 1000 });
|
|
678
|
-
expect(result.cancelled).toBe(false);
|
|
679
|
-
});
|
|
680
|
-
it('should wait for parent task to reach finalized state after child completion', async () => {
|
|
681
|
-
const parent = await queue.enqueue('parent', { value: 'parent' });
|
|
682
|
-
const dParent = await queue.dequeue({ types: ['parent'] });
|
|
683
|
-
expect(dParent).toBeDefined();
|
|
684
|
-
// Spawn a child
|
|
685
|
-
const [child] = await queue.enqueueMany([{ type: 'child', data: { value: 'child' }, parentId: parent.id, completeAfter: [] }], { returnTasks: true });
|
|
686
|
-
expect(child).toBeDefined();
|
|
687
|
-
// Re-enqueuing with dependency:
|
|
688
|
-
const parentWithDep = await queue.enqueue('parent-dep', { value: 'parent' }, { completeAfter: [child.id] });
|
|
689
|
-
let dParent2;
|
|
690
|
-
for (let i = 0; i < 10; i++) {
|
|
691
|
-
dParent2 = await queue.dequeue({ types: ['parent-dep'] });
|
|
692
|
-
if (dParent2)
|
|
693
|
-
break;
|
|
694
|
-
await timeout(50);
|
|
695
|
-
}
|
|
696
|
-
expect(dParent2?.id).toBe(parentWithDep.id);
|
|
697
|
-
// Complete parent (it will move to Waiting because of completeAfter)
|
|
698
|
-
await queue.complete(dParent2);
|
|
699
|
-
const checkParent = await queue.getTask(parentWithDep.id);
|
|
700
|
-
expect(checkParent?.status).toBe(TaskStatus.WaitingChildren);
|
|
701
|
-
void (async () => {
|
|
702
|
-
await timeout(100);
|
|
703
|
-
const dChild = await queue.dequeue({ types: ['child'] });
|
|
704
|
-
await queue.complete(dChild);
|
|
705
|
-
})();
|
|
706
|
-
await queue.waitForTasks([parentWithDep.id], { timeout: 2000 });
|
|
707
|
-
const finalParent = await queue.getTask(parentWithDep.id);
|
|
708
|
-
expect(finalParent?.status).toBe(TaskStatus.Completed);
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
describe('Bugs and Edge Cases', () => {
|
|
712
|
-
it('should NOT overwrite terminal states during cancellation (Bug 3)', async () => {
|
|
713
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
714
|
-
// 1. Enqueue parent and two children
|
|
715
|
-
const parent = await q.enqueue('parent', {});
|
|
716
|
-
const child1 = await q.enqueue('child', {}, { parentId: parent.id });
|
|
717
|
-
const child2 = await q.enqueue('child', {}, { parentId: parent.id });
|
|
718
|
-
// 2. Complete child1
|
|
719
|
-
const dequeued1 = await q.dequeue({ types: ['child'] });
|
|
720
|
-
await q.complete(dequeued1);
|
|
721
|
-
const finished1 = await q.getTask(child1.id);
|
|
722
|
-
expect(finished1?.status).toBe(TaskStatus.Completed);
|
|
723
|
-
// 3. Cancel parent tree
|
|
724
|
-
await q.cancel(parent.id);
|
|
725
|
-
// 4. Verify child1 is STILL Completed, not Cancelled
|
|
726
|
-
const updated1 = await q.getTask(child1.id);
|
|
727
|
-
expect(updated1?.status).toBe(TaskStatus.Completed);
|
|
728
|
-
// 5. Verify parent and child2 ARE Cancelled
|
|
729
|
-
const updatedParent = await q.getTask(parent.id);
|
|
730
|
-
const updated2 = await q.getTask(child2.id);
|
|
731
|
-
expect(updatedParent?.status).toBe(TaskStatus.Cancelled);
|
|
732
|
-
expect(updated2?.status).toBe(TaskStatus.Cancelled);
|
|
733
|
-
await q.clear();
|
|
734
|
-
});
|
|
735
|
-
it('should expire tasks in Waiting status (Bug 5)', async () => {
|
|
736
|
-
const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
|
|
737
|
-
// 1. Enqueue task A (will never complete)
|
|
738
|
-
const taskA = await q.enqueue('test', {});
|
|
739
|
-
// 2. Enqueue task B with scheduleAfter: [A.id] and short TTL
|
|
740
|
-
const taskB = await q.enqueue('test', {}, {
|
|
741
|
-
scheduleAfter: [taskA.id],
|
|
742
|
-
timeToLive: 100, // 100ms
|
|
743
|
-
});
|
|
744
|
-
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
745
|
-
// 3. Wait for TTL to pass
|
|
746
|
-
await timeout(200);
|
|
747
|
-
// 4. Run maintenance
|
|
748
|
-
await q.maintenance();
|
|
749
|
-
// 5. Verify task B is Expired
|
|
750
|
-
await q.waitForTasks([taskB.id]);
|
|
751
|
-
const updatedB = await q.getTask(taskB.id);
|
|
752
|
-
expect(updatedB?.status).toBe(TaskStatus.Expired);
|
|
753
|
-
await q.clear();
|
|
754
|
-
});
|
|
755
|
-
});
|
|
756
|
-
});
|