@tstdl/base 0.93.181 → 0.93.183
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 +8 -3
- 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 +13 -5
- 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 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import { CancellationSignal } from '../../cancellation/token.js';
|
|
3
|
-
import { DeferredPromise } from '../../promise/deferred-promise.js';
|
|
4
|
-
import { TaskProcessResult, TaskQueueProvider } from '../../task-queue/index.js';
|
|
5
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
6
|
-
import { timeout } from '../../utils/timing.js';
|
|
7
|
-
describe('Graceful Worker Shutdown', () => {
|
|
8
|
-
let injector;
|
|
9
|
-
let queue;
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
12
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
-
queue = queueProvider.get(`shutdown-queue-${crypto.randomUUID()}`);
|
|
14
|
-
});
|
|
15
|
-
afterAll(async () => {
|
|
16
|
-
await injector?.dispose();
|
|
17
|
-
});
|
|
18
|
-
it('should wait for active handler to finish on disposal', async () => {
|
|
19
|
-
const cancellationSignal = injector.resolve(CancellationSignal);
|
|
20
|
-
await queue.enqueue('shutdown-test', {});
|
|
21
|
-
let handlerFinished = false;
|
|
22
|
-
const handlerStarted = new DeferredPromise();
|
|
23
|
-
queue.process({ cancellationSignal }, async () => {
|
|
24
|
-
handlerStarted.resolve();
|
|
25
|
-
await timeout(300); // simulate work
|
|
26
|
-
handlerFinished = true;
|
|
27
|
-
return TaskProcessResult.Complete();
|
|
28
|
-
});
|
|
29
|
-
queue.notify();
|
|
30
|
-
// Wait for worker to pick it up
|
|
31
|
-
await handlerStarted;
|
|
32
|
-
// Call disposal
|
|
33
|
-
const disposePromise = injector.dispose();
|
|
34
|
-
// Check that disposal waits for the handler
|
|
35
|
-
expect(handlerFinished).toBe(false);
|
|
36
|
-
await disposePromise;
|
|
37
|
-
expect(handlerFinished).toBe(true);
|
|
38
|
-
// Note: We can't call queue.getTask here because pool is closed.
|
|
39
|
-
// But handlerFinished = true and successful disposePromise already prove it waited.
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { describe, expectTypeOf, test } from 'vitest';
|
|
2
|
-
describe('Task Discriminated Union', () => {
|
|
3
|
-
test('Task should be a discriminated union based on definition map', () => {
|
|
4
|
-
// Should be a union
|
|
5
|
-
expectTypeOf().toBeObject();
|
|
6
|
-
// Discrimination check
|
|
7
|
-
const task = {};
|
|
8
|
-
if (task.type == 'test-task') {
|
|
9
|
-
expectTypeOf(task.data).toEqualTypeOf();
|
|
10
|
-
expectTypeOf(task.state).toEqualTypeOf();
|
|
11
|
-
expectTypeOf(task.result).toEqualTypeOf();
|
|
12
|
-
}
|
|
13
|
-
else if (task.type == 'other-task') {
|
|
14
|
-
expectTypeOf(task.data).toEqualTypeOf();
|
|
15
|
-
expectTypeOf(task.result).toEqualTypeOf();
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
-
describe('Task Queue Transactions', () => {
|
|
5
|
-
let injector;
|
|
6
|
-
let queue;
|
|
7
|
-
beforeAll(async () => {
|
|
8
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
9
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
10
|
-
queue = queueProvider.get(`tx-queue-${crypto.randomUUID()}`);
|
|
11
|
-
});
|
|
12
|
-
afterAll(async () => {
|
|
13
|
-
await injector?.dispose();
|
|
14
|
-
});
|
|
15
|
-
it('should respect transaction rollbacks', async () => {
|
|
16
|
-
let taskId;
|
|
17
|
-
await queue.transaction(async (tx) => {
|
|
18
|
-
const task = await queue.enqueue('tx-task', { foo: 'bar' }, { transaction: tx });
|
|
19
|
-
taskId = task.id;
|
|
20
|
-
const hasTask = await queue.has(taskId, { transaction: tx });
|
|
21
|
-
expect(hasTask).toBe(true);
|
|
22
|
-
await tx.rollback();
|
|
23
|
-
});
|
|
24
|
-
const hasTaskAfterRollback = await queue.has(taskId);
|
|
25
|
-
expect(hasTaskAfterRollback).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
it('should participate in external transaction (commit)', async () => {
|
|
28
|
-
let taskId;
|
|
29
|
-
await queue.transaction(async (tx) => {
|
|
30
|
-
const task = await queue.enqueue('tx-commit', { foo: 'bar' }, { transaction: tx });
|
|
31
|
-
taskId = task.id;
|
|
32
|
-
});
|
|
33
|
-
const hasTaskAfterCommit = await queue.has(taskId);
|
|
34
|
-
expect(hasTaskAfterCommit).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
it('should handle multiple operations in one transaction', async () => {
|
|
37
|
-
await queue.transaction(async (tx) => {
|
|
38
|
-
const t1 = await queue.enqueue('t1', {}, { transaction: tx });
|
|
39
|
-
const t2 = await queue.enqueue('t2', {}, { transaction: tx });
|
|
40
|
-
await queue.cancel(t1.id, { transaction: tx });
|
|
41
|
-
const u1 = await queue.getTask(t1.id, { transaction: tx });
|
|
42
|
-
expect(u1?.status).toBe(TaskStatus.Cancelled);
|
|
43
|
-
const u2 = await queue.getTask(t2.id, { transaction: tx });
|
|
44
|
-
expect(u2?.status).toBe(TaskStatus.Pending);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { describe, expectTypeOf, test } from 'vitest';
|
|
2
|
-
describe('TaskQueue Type Definitions', () => {
|
|
3
|
-
test('TaskDefinition and TaskDefinitionMap should be defined', () => {
|
|
4
|
-
expectTypeOf().toEqualTypeOf();
|
|
5
|
-
expectTypeOf().toEqualTypeOf();
|
|
6
|
-
expectTypeOf().toEqualTypeOf();
|
|
7
|
-
expectTypeOf().toEqualTypeOf();
|
|
8
|
-
});
|
|
9
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { CancellationToken } from '../../cancellation/index.js';
|
|
3
|
-
import { getRepository } from '../../orm/server/index.js';
|
|
4
|
-
import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
5
|
-
import { PostgresTask } from '../../task-queue/postgres/index.js';
|
|
6
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
7
|
-
import { timeout } from '../../utils/timing.js';
|
|
8
|
-
describe('Worker & Base Class Tests', () => {
|
|
9
|
-
let injector;
|
|
10
|
-
let queue;
|
|
11
|
-
let token;
|
|
12
|
-
let otherQueueName;
|
|
13
|
-
let taskRepository;
|
|
14
|
-
beforeAll(async () => {
|
|
15
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
16
|
-
taskRepository = injector.resolve(getRepository(PostgresTask));
|
|
17
|
-
otherQueueName = `other-queue-${crypto.randomUUID()}`;
|
|
18
|
-
});
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
21
|
-
const queueName = `worker-queue-${crypto.randomUUID()}`;
|
|
22
|
-
queue = queueProvider.get(queueName, {
|
|
23
|
-
visibilityTimeout: 200, // Short visibility for testing lease loss
|
|
24
|
-
});
|
|
25
|
-
token = new CancellationToken();
|
|
26
|
-
});
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
token.set();
|
|
29
|
-
const namespace = queue.getTransactionalContextData().namespace;
|
|
30
|
-
// Clear foreign keys
|
|
31
|
-
await taskRepository.updateManyByQuery({ namespace }, { parentId: null });
|
|
32
|
-
await taskRepository.updateManyByQuery({ namespace: otherQueueName }, { parentId: null });
|
|
33
|
-
await queue.clear();
|
|
34
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
35
|
-
await queueProvider.get(otherQueueName).clear();
|
|
36
|
-
vi.restoreAllMocks();
|
|
37
|
-
});
|
|
38
|
-
afterAll(async () => {
|
|
39
|
-
await injector?.dispose();
|
|
40
|
-
});
|
|
41
|
-
it('should process tasks using process() helper', async () => {
|
|
42
|
-
const t1 = await queue.enqueue('work', { val: 1 });
|
|
43
|
-
const t2 = await queue.enqueue('work', { val: 2 });
|
|
44
|
-
const processed = [];
|
|
45
|
-
queue.process({ cancellationSignal: token }, async (context) => {
|
|
46
|
-
processed.push(context.data['val']);
|
|
47
|
-
return TaskProcessResult.Complete();
|
|
48
|
-
});
|
|
49
|
-
// Wait until 2 tasks are processed
|
|
50
|
-
for (let i = 0; i < 50; i++) {
|
|
51
|
-
if (processed.length == 2)
|
|
52
|
-
break;
|
|
53
|
-
queue.notify();
|
|
54
|
-
await timeout(20);
|
|
55
|
-
}
|
|
56
|
-
token.set(); // Stop worker
|
|
57
|
-
await queue.waitForTasks([t1.id, t2.id], { interval: 50, timeout: 1000 });
|
|
58
|
-
expect(processed).toContain(1);
|
|
59
|
-
expect(processed).toContain(2);
|
|
60
|
-
expect(processed.length).toBe(2);
|
|
61
|
-
const check1 = await queue.getTask(t1.id);
|
|
62
|
-
const check2 = await queue.getTask(t2.id);
|
|
63
|
-
expect(check1?.status).toBe(TaskStatus.Completed);
|
|
64
|
-
expect(check2?.status).toBe(TaskStatus.Completed);
|
|
65
|
-
});
|
|
66
|
-
it('should handle errors in worker gracefully', async () => {
|
|
67
|
-
const task = await queue.enqueue('fail', {});
|
|
68
|
-
queue.process({ cancellationSignal: token }, async () => {
|
|
69
|
-
throw new Error('worker error');
|
|
70
|
-
});
|
|
71
|
-
// Wait until task is processed (error recorded and status is Retrying)
|
|
72
|
-
for (let i = 0; i < 50; i++) {
|
|
73
|
-
const updated = await queue.getTask(task.id);
|
|
74
|
-
if (updated?.tries == 1 && updated.status == TaskStatus.Retrying) {
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
queue.notify();
|
|
78
|
-
await timeout(20);
|
|
79
|
-
}
|
|
80
|
-
token.set();
|
|
81
|
-
const updated = await queue.getTask(task.id);
|
|
82
|
-
expect(updated?.status).toBe(TaskStatus.Retrying); // Should retry
|
|
83
|
-
expect(updated?.tries).toBe(1);
|
|
84
|
-
expect(updated?.errors[0]?.message).toBe('worker error');
|
|
85
|
-
});
|
|
86
|
-
it('should extend lease (heartbeat) during long processing', async () => {
|
|
87
|
-
const task = await queue.enqueue('long', {});
|
|
88
|
-
let executed = false;
|
|
89
|
-
queue.process({ cancellationSignal: token }, async (_context) => {
|
|
90
|
-
// Simulate long work > visibilityTimeout (200ms)
|
|
91
|
-
await timeout(300);
|
|
92
|
-
executed = true;
|
|
93
|
-
return TaskProcessResult.Complete();
|
|
94
|
-
});
|
|
95
|
-
queue.notify();
|
|
96
|
-
await queue.waitForTasks([task.id], { timeout: 5000 });
|
|
97
|
-
token.set();
|
|
98
|
-
expect(executed).toBe(true);
|
|
99
|
-
const updated = await queue.getTask(task.id);
|
|
100
|
-
expect(updated?.status).toBe(TaskStatus.Completed);
|
|
101
|
-
});
|
|
102
|
-
it('should handle TaskResult actions (Fail, Reschedule)', async () => {
|
|
103
|
-
const tFail = await queue.enqueue('fail-action', {});
|
|
104
|
-
const tResched = await queue.enqueue('resched-action', {});
|
|
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 TaskProcessResult.Fail(new Error('explicit fail'));
|
|
110
|
-
}
|
|
111
|
-
if (context.id == tResched.id) {
|
|
112
|
-
return TaskProcessResult.RescheduleBy(1000);
|
|
113
|
-
}
|
|
114
|
-
return TaskProcessResult.Complete();
|
|
115
|
-
});
|
|
116
|
-
// Wait until tasks are processed (error/reschedule recorded and status is Retrying/Pending)
|
|
117
|
-
for (let i = 0; i < 50; i++) {
|
|
118
|
-
const uFail = await queue.getTask(tFail.id);
|
|
119
|
-
const uResched = await queue.getTask(tResched.id);
|
|
120
|
-
if (uFail?.tries == 1 && uFail.status == TaskStatus.Retrying && uResched?.status == TaskStatus.Pending && (uResched?.scheduleTimestamp ?? 0) > Date.now()) {
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
queue.notify();
|
|
124
|
-
await timeout(20);
|
|
125
|
-
}
|
|
126
|
-
token.set();
|
|
127
|
-
const uFail = await queue.getTask(tFail.id);
|
|
128
|
-
expect(uFail?.status).toBe(TaskStatus.Retrying); // Retry
|
|
129
|
-
expect(uFail?.errors[0]?.message).toBe('explicit fail');
|
|
130
|
-
const uResched = await queue.getTask(tResched.id);
|
|
131
|
-
expect(uResched?.status).toBe(TaskStatus.Pending);
|
|
132
|
-
expect(uResched?.scheduleTimestamp).toBeGreaterThan(Date.now());
|
|
133
|
-
});
|
|
134
|
-
it('should exercise TaskContext methods', async () => {
|
|
135
|
-
const task = await queue.enqueue('context-test', { val: 1 });
|
|
136
|
-
let executed = false;
|
|
137
|
-
queue.process({ cancellationSignal: token, types: ['context-test'] }, async (context) => {
|
|
138
|
-
expect(context.id).toBe(task.id);
|
|
139
|
-
expect(context.data).toEqual({ val: 1 });
|
|
140
|
-
expect(context.attempt).toBe(1);
|
|
141
|
-
expect(context.triesLeft).toBeGreaterThan(0);
|
|
142
|
-
expect(context.logger).toBeDefined();
|
|
143
|
-
expect(context.signal).toBeDefined();
|
|
144
|
-
await context.checkpoint({ progress: 0.5 });
|
|
145
|
-
const child = await context.spawn('child', { c: 1 });
|
|
146
|
-
expect(child.parentId).toBe(task.id);
|
|
147
|
-
const children = await context.spawnMany([{ type: 'child', data: { c: 2 } }]);
|
|
148
|
-
expect(children[0]?.parentId).toBe(task.id);
|
|
149
|
-
// Other queue spawn
|
|
150
|
-
const otherQueue = injector.resolve(TaskQueueProvider).get(otherQueueName);
|
|
151
|
-
const otherChild = await context.spawn(otherQueue, 'other', { x: 1 });
|
|
152
|
-
expect(otherChild.namespace).toBe(otherQueueName);
|
|
153
|
-
await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
|
|
154
|
-
executed = true;
|
|
155
|
-
return TaskProcessResult.Complete();
|
|
156
|
-
});
|
|
157
|
-
// Complete children so parent can finalize
|
|
158
|
-
void (async () => {
|
|
159
|
-
while (!executed) {
|
|
160
|
-
await timeout(50);
|
|
161
|
-
queue.notify();
|
|
162
|
-
}
|
|
163
|
-
// At this point parent should be WaitingChildren if children are not done
|
|
164
|
-
await queue.waitForTasks([task.id], { statuses: [TaskStatus.WaitingChildren], timeout: 2000 });
|
|
165
|
-
const midTask = await queue.getTask(task.id);
|
|
166
|
-
expect(midTask?.status).toBe(TaskStatus.WaitingChildren);
|
|
167
|
-
while (true) {
|
|
168
|
-
const dChild = await queue.dequeue({ types: ['child'] });
|
|
169
|
-
if (!dChild) {
|
|
170
|
-
// Check other-queue as well
|
|
171
|
-
const otherQueue = injector.resolve(TaskQueueProvider).get(otherQueueName);
|
|
172
|
-
const dOther = await otherQueue.dequeue();
|
|
173
|
-
if (dOther) {
|
|
174
|
-
await otherQueue.complete(dOther);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
await queue.complete(dChild);
|
|
180
|
-
}
|
|
181
|
-
})();
|
|
182
|
-
queue.notify();
|
|
183
|
-
await queue.waitForTasks([task.id], { interval: 50, timeout: 2000 });
|
|
184
|
-
token.set();
|
|
185
|
-
expect(executed).toBe(true);
|
|
186
|
-
const finalTask = await queue.getTask(task.id);
|
|
187
|
-
expect(finalTask?.status).toBe(TaskStatus.Completed);
|
|
188
|
-
});
|
|
189
|
-
it('should correctly report isFinalAttempt in TaskContext', async () => {
|
|
190
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
191
|
-
const queueName = `final-try-test-${crypto.randomUUID()}`;
|
|
192
|
-
const testQueue = queueProvider.get(queueName, {
|
|
193
|
-
maxTries: 2,
|
|
194
|
-
retryDelayMinimum: 0,
|
|
195
|
-
retryDelayGrowth: 1,
|
|
196
|
-
});
|
|
197
|
-
await testQueue.enqueue('work', {});
|
|
198
|
-
testQueue.notify();
|
|
199
|
-
const finalAttemptValues = [];
|
|
200
|
-
testQueue.process({ cancellationSignal: token }, async (context) => {
|
|
201
|
-
finalAttemptValues.push(context.isFinalAttempt);
|
|
202
|
-
if (context.attempt == 1) {
|
|
203
|
-
throw new Error('fail first attempt');
|
|
204
|
-
}
|
|
205
|
-
return TaskProcessResult.Complete();
|
|
206
|
-
});
|
|
207
|
-
for (let i = 0; i < 100; i++) {
|
|
208
|
-
if (finalAttemptValues.length == 2)
|
|
209
|
-
break;
|
|
210
|
-
testQueue.notify();
|
|
211
|
-
await timeout(20);
|
|
212
|
-
}
|
|
213
|
-
token.set();
|
|
214
|
-
expect(finalAttemptValues).toEqual([false, true]);
|
|
215
|
-
await testQueue.clear();
|
|
216
|
-
});
|
|
217
|
-
it('should handle unsupported task result action', async () => {
|
|
218
|
-
const task = await queue.enqueue('unsupported-action', {});
|
|
219
|
-
const workerPromise = queue.processWorker(token, () => ({ payload: { action: 'magic' } }));
|
|
220
|
-
await expect(workerPromise).rejects.toThrow('Unsupported task result action');
|
|
221
|
-
const updated = await queue.getTask(task.id);
|
|
222
|
-
expect(updated?.status).toBe(TaskStatus.Dead); // Fatal now
|
|
223
|
-
expect(updated?.errors[0]?.message).toContain('Unsupported task result action');
|
|
224
|
-
});
|
|
225
|
-
it('should not crash worker lane if fail() throws', async () => {
|
|
226
|
-
const t1 = await queue.enqueue('task1', {});
|
|
227
|
-
const t2 = await queue.enqueue('task2', {});
|
|
228
|
-
let t1Processed = false;
|
|
229
|
-
let t2Processed = false;
|
|
230
|
-
// Mock fail to throw once
|
|
231
|
-
const failSpy = vi.spyOn(queue, 'fail').mockRejectedValueOnce(new Error('DB DOWN'));
|
|
232
|
-
queue.process({ cancellationSignal: token }, async (context) => {
|
|
233
|
-
if (context.id === t1.id) {
|
|
234
|
-
t1Processed = true;
|
|
235
|
-
throw new Error('Task 1 failed');
|
|
236
|
-
}
|
|
237
|
-
if (context.id === t2.id) {
|
|
238
|
-
t2Processed = true;
|
|
239
|
-
return TaskProcessResult.Complete();
|
|
240
|
-
}
|
|
241
|
-
return TaskProcessResult.Complete();
|
|
242
|
-
});
|
|
243
|
-
// Wait until both tasks are processed
|
|
244
|
-
// If the bug exists, the worker lane will crash after t1 and t2 will never be processed
|
|
245
|
-
for (let i = 0; i < 40; i++) {
|
|
246
|
-
if (t1Processed && t2Processed)
|
|
247
|
-
break;
|
|
248
|
-
queue.notify();
|
|
249
|
-
await timeout(50);
|
|
250
|
-
}
|
|
251
|
-
token.set();
|
|
252
|
-
expect(t1Processed).toBe(true);
|
|
253
|
-
expect(t2Processed).toBe(true);
|
|
254
|
-
expect(failSpy).toHaveBeenCalled();
|
|
255
|
-
const check2 = await queue.getTask(t2.id);
|
|
256
|
-
expect(check2?.status).toBe(TaskStatus.Completed);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
-
import { timeout } from '../../utils/timing.js';
|
|
5
|
-
describe('Zombie Parent Deadlock', () => {
|
|
6
|
-
let injector;
|
|
7
|
-
let queue;
|
|
8
|
-
beforeAll(async () => {
|
|
9
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
10
|
-
});
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
-
const queueName = `zombie-queue-${crypto.randomUUID()}`;
|
|
14
|
-
queue = queueProvider.get(queueName, {
|
|
15
|
-
visibilityTimeout: 1000,
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
afterEach(async () => {
|
|
19
|
-
await queue.clear();
|
|
20
|
-
});
|
|
21
|
-
afterAll(async () => {
|
|
22
|
-
await injector?.dispose();
|
|
23
|
-
});
|
|
24
|
-
it('should resolve parent even if child fails (abortOnDependencyFailure: false)', async () => {
|
|
25
|
-
const parent = await queue.enqueue('parent', {});
|
|
26
|
-
const dParent = await queue.dequeue();
|
|
27
|
-
// Spawn a child that will fail. Parent has abortOnDependencyFailure: false by default.
|
|
28
|
-
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, abortOnDependencyFailure: false }], { returnTasks: true });
|
|
29
|
-
await queue.complete(dParent);
|
|
30
|
-
// Parent should be WaitingChildren
|
|
31
|
-
const uParent = await queue.getTask(parent.id);
|
|
32
|
-
expect(uParent?.status).toBe(TaskStatus.WaitingChildren);
|
|
33
|
-
// Fail the child fatally
|
|
34
|
-
const dChild = await queue.dequeue();
|
|
35
|
-
await queue.fail(dChild, new Error('child failed'), { fatal: true });
|
|
36
|
-
// Verify child is Dead
|
|
37
|
-
const uChild = await queue.getTask(child.id);
|
|
38
|
-
expect(uChild?.status).toBe(TaskStatus.Dead);
|
|
39
|
-
// Wait a bit for dependency resolution
|
|
40
|
-
await timeout(200);
|
|
41
|
-
// Parent should NOT be stuck. It should transition to Completed because the child is terminal.
|
|
42
|
-
const fParent = await queue.getTask(parent.id);
|
|
43
|
-
expect(fParent?.status).toBe(TaskStatus.Completed);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
|
-
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
-
import { timeout } from '../../utils/timing.js';
|
|
5
|
-
describe('Zombie Recovery Race Condition', () => {
|
|
6
|
-
let injector;
|
|
7
|
-
let queue;
|
|
8
|
-
beforeAll(async () => {
|
|
9
|
-
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
10
|
-
});
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
-
const queueName = `zombie-recovery-queue-${crypto.randomUUID()}`;
|
|
14
|
-
queue = queueProvider.get(queueName, {
|
|
15
|
-
visibilityTimeout: 100, // Very short visibility timeout
|
|
16
|
-
retryDelayMinimum: 0,
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await queue.clear();
|
|
21
|
-
});
|
|
22
|
-
afterAll(async () => {
|
|
23
|
-
await injector?.dispose();
|
|
24
|
-
});
|
|
25
|
-
it('should NOT prematurely complete a recovered parent when children finish', async () => {
|
|
26
|
-
// 1. Enqueue parent
|
|
27
|
-
const parent = await queue.enqueue('parent', {});
|
|
28
|
-
// 2. Dequeue parent (it is now Running, startTimestamp is set)
|
|
29
|
-
const dParent = await queue.dequeue();
|
|
30
|
-
expect(dParent?.id).toBe(parent.id);
|
|
31
|
-
expect(dParent?.startTimestamp).not.toBeNull();
|
|
32
|
-
// 3. Parent spawns a child that it waits for
|
|
33
|
-
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id }], { returnTasks: true });
|
|
34
|
-
// 4. Simulate crash: Wait for visibility timeout so it becomes a zombie
|
|
35
|
-
await timeout(200);
|
|
36
|
-
// 5. Run maintenance to recover the zombie parent
|
|
37
|
-
await queue.maintenance();
|
|
38
|
-
// Verify it was recovered to Pending
|
|
39
|
-
const recoveredParent = await queue.getTask(parent.id);
|
|
40
|
-
expect(recoveredParent?.status).toBe(TaskStatus.Retrying);
|
|
41
|
-
// If the bug exists, startTimestamp is still set here.
|
|
42
|
-
// 6. Complete the child while parent is still Pending
|
|
43
|
-
const dChild = await queue.dequeue({ types: ['child'] });
|
|
44
|
-
await queue.complete(dChild);
|
|
45
|
-
// 7. Verify parent status
|
|
46
|
-
// BUG: evaluateTaskStatus will see unresolvedCompleteDependencies=0 and startTimestamp != null,
|
|
47
|
-
// and incorrectly transition the Pending parent to Completed.
|
|
48
|
-
const finalParent = await queue.getTask(parent.id);
|
|
49
|
-
expect(finalParent?.status).toBe(TaskStatus.Retrying); // It should still be pending execution!
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { BackoffHelper } from '../backoff.js';
|
|
3
|
-
describe('BackoffHelper', () => {
|
|
4
|
-
it('should support full jitter', () => {
|
|
5
|
-
const helper = new BackoffHelper({
|
|
6
|
-
initialDelay: 1000,
|
|
7
|
-
jitter: 1,
|
|
8
|
-
jitterStrategy: 'full',
|
|
9
|
-
});
|
|
10
|
-
for (let i = 0; i < 100; i++) {
|
|
11
|
-
const delay = helper.getNextDelay();
|
|
12
|
-
// Full jitter should be between 0 and the base delay (1000 for first call, then 2000, 4000, etc.)
|
|
13
|
-
expect(delay).toBeGreaterThanOrEqual(0);
|
|
14
|
-
expect(delay).toBeLessThanOrEqual(1000 * Math.pow(2, i));
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
it('should support proportional jitter (default)', () => {
|
|
18
|
-
const helper = new BackoffHelper({
|
|
19
|
-
initialDelay: 1000,
|
|
20
|
-
jitter: 0.1,
|
|
21
|
-
jitterStrategy: 'proportional',
|
|
22
|
-
});
|
|
23
|
-
for (let i = 0; i < 1; i++) {
|
|
24
|
-
const delay = helper.getNextDelay();
|
|
25
|
-
// Proportional jitter should be 1000 ± 100 (900 to 1100)
|
|
26
|
-
expect(delay).toBeGreaterThanOrEqual(900);
|
|
27
|
-
expect(delay).toBeLessThanOrEqual(1100);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
it('should provide status emission', () => {
|
|
31
|
-
const helper = new BackoffHelper({ initialDelay: 1000 });
|
|
32
|
-
expect(helper.status.attempt).toBe(0);
|
|
33
|
-
expect(helper.status.currentDelay).toBe(1000);
|
|
34
|
-
helper.getNextDelay();
|
|
35
|
-
expect(helper.status.attempt).toBe(1);
|
|
36
|
-
expect(helper.status.currentDelay).toBe(2000);
|
|
37
|
-
helper.reset();
|
|
38
|
-
expect(helper.status.attempt).toBe(0);
|
|
39
|
-
expect(helper.status.currentDelay).toBe(1000);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { TimeoutError } from '../../errors/timeout.error.js';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { retryWithBackoff } from '../retry-with-backoff.js';
|
|
4
|
-
describe('retryWithBackoff', () => {
|
|
5
|
-
it('should return result on success', async () => {
|
|
6
|
-
const result = await retryWithBackoff(async () => 'success');
|
|
7
|
-
expect(result).toBe('success');
|
|
8
|
-
});
|
|
9
|
-
it('should retry on failure if shouldRetry returns true', async () => {
|
|
10
|
-
let attempts = 0;
|
|
11
|
-
const result = await retryWithBackoff(async () => {
|
|
12
|
-
attempts++;
|
|
13
|
-
if (attempts < 3) {
|
|
14
|
-
throw new Error('fail');
|
|
15
|
-
}
|
|
16
|
-
return 'success';
|
|
17
|
-
}, {
|
|
18
|
-
retry: {
|
|
19
|
-
shouldRetry: (_error, attempt) => attempt < 5,
|
|
20
|
-
backoff: { initialDelay: 0 },
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
expect(result).toBe('success');
|
|
24
|
-
expect(attempts).toBe(3);
|
|
25
|
-
});
|
|
26
|
-
it('should throw after max retries', async () => {
|
|
27
|
-
let attempts = 0;
|
|
28
|
-
const execute = retryWithBackoff(async () => {
|
|
29
|
-
attempts++;
|
|
30
|
-
throw new Error('fail');
|
|
31
|
-
}, {
|
|
32
|
-
retry: {
|
|
33
|
-
shouldRetry: (_error, attempt) => attempt < 2,
|
|
34
|
-
backoff: { initialDelay: 0 },
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
await expect(execute).rejects.toThrow('fail');
|
|
38
|
-
expect(attempts).toBe(3); // 0, 1, 2
|
|
39
|
-
});
|
|
40
|
-
it('should timeout if execution takes too long', async () => {
|
|
41
|
-
const execute = retryWithBackoff(async () => {
|
|
42
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
43
|
-
return 'success';
|
|
44
|
-
}, {
|
|
45
|
-
timeout: 50,
|
|
46
|
-
});
|
|
47
|
-
await expect(execute).rejects.toThrow(TimeoutError);
|
|
48
|
-
});
|
|
49
|
-
});
|