@tstdl/base 0.93.139 → 0.93.141
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/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.d.ts +1 -1
- package/application/application.js +3 -3
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/README.md +267 -0
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/README.md +249 -0
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +3 -4
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +9 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/README.md +305 -0
- package/reflection/decorator-data.js +11 -12
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +293 -0
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +4 -13
- package/task-queue/postgres/task-queue.js +462 -355
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +8 -8
- package/task-queue/task-queue.d.ts +53 -19
- package/task-queue/task-queue.js +121 -55
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +45 -229
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +28 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +128 -8
- package/task-queue/tests/worker.test.js +39 -16
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/templates/README.md +287 -0
- package/test5.js +5 -5
- package/testing/README.md +157 -0
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/README.md +346 -0
- package/text/localization.service.js +2 -2
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/file-reader.js +1 -2
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { CancellationToken } from '../../cancellation/index.js';
|
|
2
3
|
import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
4
|
import { setupIntegrationTest } from '../../testing/index.js';
|
|
4
5
|
import { currentTimestamp } from '../../utils/date-time.js';
|
|
6
|
+
import { Timer } from '../../utils/timer.js';
|
|
5
7
|
import { timeout } from '../../utils/timing.js';
|
|
8
|
+
import { isDefined } from '../../utils/type-guards.js';
|
|
6
9
|
describe('Queue Integration Tests', () => {
|
|
7
10
|
let injector;
|
|
8
11
|
let queue;
|
|
9
|
-
const queueName = `test-queue-${Date.now()}`;
|
|
10
12
|
// Helper to verify state in DB
|
|
11
13
|
async function assertTaskStatus(id, state, message) {
|
|
12
14
|
const task = await queue.getTask(id);
|
|
@@ -14,10 +16,10 @@ describe('Queue Integration Tests', () => {
|
|
|
14
16
|
}
|
|
15
17
|
beforeAll(async () => {
|
|
16
18
|
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// messing with `injectArgument` manually in the test body.
|
|
19
|
+
});
|
|
20
|
+
beforeEach(async () => {
|
|
20
21
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
22
|
+
const queueName = `test-queue-${crypto.randomUUID()}`;
|
|
21
23
|
queue = queueProvider.get(queueName, {
|
|
22
24
|
visibilityTimeout: 5000,
|
|
23
25
|
retryDelayMinimum: 100, // Fast retries for testing
|
|
@@ -30,7 +32,9 @@ describe('Queue Integration Tests', () => {
|
|
|
30
32
|
});
|
|
31
33
|
afterAll(async () => {
|
|
32
34
|
try {
|
|
33
|
-
|
|
35
|
+
if (isDefined(queue)) {
|
|
36
|
+
await queue.clear();
|
|
37
|
+
}
|
|
34
38
|
await injector.dispose();
|
|
35
39
|
}
|
|
36
40
|
catch (error) {
|
|
@@ -74,9 +78,9 @@ describe('Queue Integration Tests', () => {
|
|
|
74
78
|
expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
|
|
75
79
|
const check1 = await queue.getTask(t1.id);
|
|
76
80
|
expect((check1?.data)['value']).toBe('original');
|
|
77
|
-
// 3. Replace Strategy: Should replace existing task with new data
|
|
81
|
+
// 3. Replace Strategy: Should replace existing task with new data. ID stays the same to avoid foreign key violations.
|
|
78
82
|
const t3 = await queue.enqueueMany([{ type: 'test', data: { value: 'updated' }, idempotencyKey: key }], { replace: true, returnTasks: true });
|
|
79
|
-
expect(t3[0].id, '
|
|
83
|
+
expect(t3[0].id, 'Same ID if replaced').toBe(t1.id);
|
|
80
84
|
// New task should have new data
|
|
81
85
|
const checkNew = await queue.getTask(t3[0].id);
|
|
82
86
|
expect((checkNew?.data)['value']).toBe('updated');
|
|
@@ -276,7 +280,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
276
280
|
const initialLock = dequeued.visibilityDeadline;
|
|
277
281
|
await timeout(20);
|
|
278
282
|
const touched = await queue.touch(dequeued);
|
|
279
|
-
expect(touched
|
|
283
|
+
expect(touched.visibilityDeadline > initialLock).toBe(true);
|
|
280
284
|
});
|
|
281
285
|
it('should prevent touching if token is lost (stolen by another worker)', async () => {
|
|
282
286
|
await queue.enqueue('foo', { foo: 'work' });
|
|
@@ -331,4 +335,120 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
331
335
|
expect(updated?.state).toEqual({ step: 1 });
|
|
332
336
|
});
|
|
333
337
|
});
|
|
338
|
+
describe('waitForTasks', () => {
|
|
339
|
+
it('should wait for multiple tasks to reach finalized state', async () => {
|
|
340
|
+
const t1 = await queue.enqueue('foo', { foo: 'wait-1' });
|
|
341
|
+
const t2 = await queue.enqueue('foo', { foo: 'wait-2' });
|
|
342
|
+
void (async () => {
|
|
343
|
+
await timeout(100);
|
|
344
|
+
const d1 = await queue.dequeue();
|
|
345
|
+
await queue.complete(d1);
|
|
346
|
+
await timeout(100);
|
|
347
|
+
const d2 = await queue.dequeue();
|
|
348
|
+
await queue.complete(d2);
|
|
349
|
+
})();
|
|
350
|
+
const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
|
|
351
|
+
expect(result.cancelled).toBe(false);
|
|
352
|
+
const check1 = await queue.getTask(t1.id);
|
|
353
|
+
const check2 = await queue.getTask(t2.id);
|
|
354
|
+
expect(check1?.status).toBe(TaskStatus.Completed);
|
|
355
|
+
expect(check2?.status).toBe(TaskStatus.Completed);
|
|
356
|
+
});
|
|
357
|
+
it('should throw TimeoutError on timeout', async () => {
|
|
358
|
+
const t1 = await queue.enqueue('foo', { foo: 'timeout' });
|
|
359
|
+
await expect(queue.waitForTasks([t1.id], { timeout: 100 })).rejects.toThrow('Timeout while waiting for tasks to complete');
|
|
360
|
+
});
|
|
361
|
+
it('should wait for Cancelled and Dead states', async () => {
|
|
362
|
+
const t1 = await queue.enqueue('foo', { foo: 'cancel' });
|
|
363
|
+
const t2 = await queue.enqueue('foo', { foo: 'dead' });
|
|
364
|
+
void (async () => {
|
|
365
|
+
await timeout(50);
|
|
366
|
+
const d1 = await queue.dequeue();
|
|
367
|
+
if (d1)
|
|
368
|
+
await queue.cancel(d1.id);
|
|
369
|
+
const d2 = await queue.dequeue();
|
|
370
|
+
if (d2)
|
|
371
|
+
await queue.fail(d2, new Error('fatal'), { fatal: true });
|
|
372
|
+
queue.notify();
|
|
373
|
+
})();
|
|
374
|
+
const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
|
|
375
|
+
expect(result.cancelled).toBe(false);
|
|
376
|
+
const c1 = await queue.getTask(t1.id);
|
|
377
|
+
const c2 = await queue.getTask(t2.id);
|
|
378
|
+
expect(c1?.status).toBe(TaskStatus.Cancelled);
|
|
379
|
+
expect(c2?.status).toBe(TaskStatus.Dead);
|
|
380
|
+
});
|
|
381
|
+
it('should handle cancellationSignal', async () => {
|
|
382
|
+
const t1 = await queue.enqueue('foo', { foo: 'long' });
|
|
383
|
+
const signal = new CancellationToken();
|
|
384
|
+
void timeout(100).then(() => signal.set());
|
|
385
|
+
const result = await queue.waitForTasks([t1.id], { cancellationSignal: signal, timeout: 5000 });
|
|
386
|
+
expect(result.cancelled).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
it('should return immediately for non-existent tasks', async () => {
|
|
389
|
+
const result = await queue.waitForTasks([crypto.randomUUID()], { timeout: 1000 });
|
|
390
|
+
expect(result.cancelled).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
it('should return immediately if all tasks are already finalized', async () => {
|
|
393
|
+
const t1 = await queue.enqueue('foo', { foo: 'immediate' });
|
|
394
|
+
const d1 = await queue.dequeue();
|
|
395
|
+
await queue.complete(d1);
|
|
396
|
+
const timer = Timer.startNew();
|
|
397
|
+
const result = await queue.waitForTasks([t1.id], { timeout: 1000 });
|
|
398
|
+
expect(result.cancelled).toBe(false);
|
|
399
|
+
expect(timer.milliseconds).toBeLessThan(100);
|
|
400
|
+
});
|
|
401
|
+
it('should handle a mix of active and archived tasks', async () => {
|
|
402
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
403
|
+
const archiveQueue = queueProvider.get(`archive-test-${crypto.randomUUID()}`, {
|
|
404
|
+
retention: 0, // Archive immediately
|
|
405
|
+
});
|
|
406
|
+
const t1 = await archiveQueue.enqueue('foo', { foo: 'archived' });
|
|
407
|
+
const d1 = await archiveQueue.dequeue();
|
|
408
|
+
await archiveQueue.complete(d1);
|
|
409
|
+
// Run maintenance to move to archive
|
|
410
|
+
await archiveQueue.maintenance();
|
|
411
|
+
const t2 = await archiveQueue.enqueue('foo', { foo: 'active' });
|
|
412
|
+
const d2 = await archiveQueue.dequeue();
|
|
413
|
+
await archiveQueue.complete(d2);
|
|
414
|
+
// t1 is archived, t2 is completed (active)
|
|
415
|
+
const result = await archiveQueue.waitForTasks([t1.id, t2.id], { timeout: 1000 });
|
|
416
|
+
expect(result.cancelled).toBe(false);
|
|
417
|
+
await archiveQueue.clear();
|
|
418
|
+
});
|
|
419
|
+
it('should return immediately for empty ids array', async () => {
|
|
420
|
+
const result = await queue.waitForTasks([], { timeout: 1000 });
|
|
421
|
+
expect(result.cancelled).toBe(false);
|
|
422
|
+
});
|
|
423
|
+
it('should wait for parent task to reach finalized state after child completion', async () => {
|
|
424
|
+
const parent = await queue.enqueue('parent', { value: 'parent' });
|
|
425
|
+
const dParent = await queue.dequeue({ types: ['parent'] });
|
|
426
|
+
expect(dParent).toBeDefined();
|
|
427
|
+
// Spawn a child
|
|
428
|
+
const [child] = await queue.enqueueMany([{ type: 'child', data: { value: 'child' }, parentId: parent.id, completeAfter: [] }], { returnTasks: true });
|
|
429
|
+
expect(child).toBeDefined();
|
|
430
|
+
// Re-enqueuing with dependency:
|
|
431
|
+
const parentWithDep = await queue.enqueue('parent-dep', { value: 'parent' }, { completeAfter: [child.id] });
|
|
432
|
+
let dParent2;
|
|
433
|
+
for (let i = 0; i < 10; i++) {
|
|
434
|
+
dParent2 = await queue.dequeue({ types: ['parent-dep'] });
|
|
435
|
+
if (dParent2)
|
|
436
|
+
break;
|
|
437
|
+
await timeout(50);
|
|
438
|
+
}
|
|
439
|
+
expect(dParent2?.id).toBe(parentWithDep.id);
|
|
440
|
+
// Complete parent (it will move to Waiting because of completeAfter)
|
|
441
|
+
await queue.complete(dParent2);
|
|
442
|
+
const checkParent = await queue.getTask(parentWithDep.id);
|
|
443
|
+
expect(checkParent?.status).toBe(TaskStatus.WaitingChildren);
|
|
444
|
+
void (async () => {
|
|
445
|
+
await timeout(100);
|
|
446
|
+
const dChild = await queue.dequeue({ types: ['child'] });
|
|
447
|
+
await queue.complete(dChild);
|
|
448
|
+
})();
|
|
449
|
+
await queue.waitForTasks([parentWithDep.id], { timeout: 2000 });
|
|
450
|
+
const finalParent = await queue.getTask(parentWithDep.id);
|
|
451
|
+
expect(finalParent?.status).toBe(TaskStatus.Completed);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
334
454
|
});
|
|
@@ -37,11 +37,12 @@ describe('Worker & Base Class Tests', () => {
|
|
|
37
37
|
});
|
|
38
38
|
// Wait until 2 tasks are processed
|
|
39
39
|
for (let i = 0; i < 50; i++) {
|
|
40
|
-
if (processed.length
|
|
40
|
+
if (processed.length == 2)
|
|
41
41
|
break;
|
|
42
42
|
await timeout(20);
|
|
43
43
|
}
|
|
44
44
|
token.set(); // Stop worker
|
|
45
|
+
await queue.waitForTasks([t1.id, t2.id], { interval: 50, timeout: 1000 });
|
|
45
46
|
expect(processed).toContain(1);
|
|
46
47
|
expect(processed).toContain(2);
|
|
47
48
|
expect(processed.length).toBe(2);
|
|
@@ -55,7 +56,14 @@ describe('Worker & Base Class Tests', () => {
|
|
|
55
56
|
queue.process({ cancellationSignal: token }, async () => {
|
|
56
57
|
throw new Error('worker error');
|
|
57
58
|
});
|
|
58
|
-
|
|
59
|
+
// Wait until task is processed (error recorded and status is Pending)
|
|
60
|
+
for (let i = 0; i < 50; i++) {
|
|
61
|
+
const updated = await queue.getTask(task.id);
|
|
62
|
+
if (updated?.tries == 1 && updated.status == TaskStatus.Pending) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
await timeout(20);
|
|
66
|
+
}
|
|
59
67
|
token.set();
|
|
60
68
|
const updated = await queue.getTask(task.id);
|
|
61
69
|
expect(updated?.status).toBe(TaskStatus.Pending); // Should retry
|
|
@@ -71,7 +79,7 @@ describe('Worker & Base Class Tests', () => {
|
|
|
71
79
|
executed = true;
|
|
72
80
|
return TaskProcessResult.Complete();
|
|
73
81
|
});
|
|
74
|
-
await timeout
|
|
82
|
+
await queue.waitForTasks([task.id], { timeout: 5000 });
|
|
75
83
|
token.set();
|
|
76
84
|
expect(executed).toBe(true);
|
|
77
85
|
const updated = await queue.getTask(task.id);
|
|
@@ -83,17 +91,21 @@ describe('Worker & Base Class Tests', () => {
|
|
|
83
91
|
const processed = new Set();
|
|
84
92
|
queue.process({ cancellationSignal: token }, async (context) => {
|
|
85
93
|
processed.add(context.id);
|
|
86
|
-
if (context.id
|
|
94
|
+
if (context.id == tFail.id) {
|
|
87
95
|
return TaskProcessResult.Fail(new Error('explicit fail'));
|
|
88
96
|
}
|
|
89
|
-
if (context.id
|
|
97
|
+
if (context.id == tResched.id) {
|
|
90
98
|
return TaskProcessResult.RescheduleBy(1000);
|
|
91
99
|
}
|
|
92
100
|
return TaskProcessResult.Complete();
|
|
93
101
|
});
|
|
102
|
+
// Wait until tasks are processed (error/reschedule recorded and status is Pending)
|
|
94
103
|
for (let i = 0; i < 50; i++) {
|
|
95
|
-
|
|
104
|
+
const uFail = await queue.getTask(tFail.id);
|
|
105
|
+
const uResched = await queue.getTask(tResched.id);
|
|
106
|
+
if (uFail?.tries == 1 && uResched?.status == TaskStatus.Pending && (uResched?.scheduleTimestamp ?? 0) > Date.now()) {
|
|
96
107
|
break;
|
|
108
|
+
}
|
|
97
109
|
await timeout(20);
|
|
98
110
|
}
|
|
99
111
|
token.set();
|
|
@@ -122,20 +134,31 @@ describe('Worker & Base Class Tests', () => {
|
|
|
122
134
|
// Other queue spawn
|
|
123
135
|
const otherQueue = injector.resolve(TaskQueueProvider).get('other-queue');
|
|
124
136
|
const otherChild = await context.spawn(otherQueue, 'other', { x: 1 });
|
|
125
|
-
expect(otherChild.parentId).toBe(task.id);
|
|
126
137
|
expect(otherChild.namespace).toBe('other-queue');
|
|
127
|
-
|
|
128
|
-
expect(otherChildren[0]?.parentId).toBe(task.id);
|
|
138
|
+
await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
|
|
129
139
|
executed = true;
|
|
130
140
|
return TaskProcessResult.Complete();
|
|
131
141
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
// Complete children so parent can finalize
|
|
143
|
+
void (async () => {
|
|
144
|
+
while (!executed) {
|
|
145
|
+
await timeout(50);
|
|
146
|
+
}
|
|
147
|
+
// At this point parent should be WaitingChildren if children are not done
|
|
148
|
+
const midTask = await queue.getTask(task.id);
|
|
149
|
+
expect(midTask?.status).toBe(TaskStatus.WaitingChildren);
|
|
150
|
+
while (true) {
|
|
151
|
+
const dChild = await queue.dequeue({ types: ['child'] });
|
|
152
|
+
if (!dChild)
|
|
153
|
+
break;
|
|
154
|
+
await queue.complete(dChild);
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
await queue.waitForTasks([task.id], { interval: 50, timeout: 5000 });
|
|
137
158
|
token.set();
|
|
138
159
|
expect(executed).toBe(true);
|
|
160
|
+
const finalTask = await queue.getTask(task.id);
|
|
161
|
+
expect(finalTask?.status).toBe(TaskStatus.Completed);
|
|
139
162
|
});
|
|
140
163
|
it('should correctly report isFinalAttempt in TaskContext', async () => {
|
|
141
164
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
@@ -150,13 +173,13 @@ describe('Worker & Base Class Tests', () => {
|
|
|
150
173
|
const finalAttemptValues = [];
|
|
151
174
|
testQueue.process({ cancellationSignal: token }, async (context) => {
|
|
152
175
|
finalAttemptValues.push(context.isFinalAttempt);
|
|
153
|
-
if (context.attempt
|
|
176
|
+
if (context.attempt == 1) {
|
|
154
177
|
throw new Error('fail first attempt');
|
|
155
178
|
}
|
|
156
179
|
return TaskProcessResult.Complete();
|
|
157
180
|
});
|
|
158
181
|
for (let i = 0; i < 100; i++) {
|
|
159
|
-
if (finalAttemptValues.length
|
|
182
|
+
if (finalAttemptValues.length == 2)
|
|
160
183
|
break;
|
|
161
184
|
testQueue.notify();
|
|
162
185
|
await timeout(20);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
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-${Date.now()}-${Math.random()}`;
|
|
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 (failFast: false)', async () => {
|
|
25
|
+
const parent = await queue.enqueue('parent', {});
|
|
26
|
+
const dParent = await queue.dequeue();
|
|
27
|
+
// Spawn a child that will fail. Parent has failFast: false by default.
|
|
28
|
+
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, failFast: 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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
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-${Date.now()}-${Math.random()}`;
|
|
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.Pending);
|
|
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.Pending); // It should still be pending execution!
|
|
50
|
+
});
|
|
51
|
+
});
|