@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.
Files changed (218) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.d.ts +1 -1
  28. package/application/application.js +3 -3
  29. package/application/providers.d.ts +20 -2
  30. package/application/providers.js +34 -7
  31. package/audit/README.md +267 -0
  32. package/audit/module.d.ts +5 -0
  33. package/audit/module.js +9 -1
  34. package/authentication/README.md +288 -0
  35. package/authentication/client/authentication.service.d.ts +12 -11
  36. package/authentication/client/authentication.service.js +21 -21
  37. package/authentication/client/http-client.middleware.js +2 -2
  38. package/authentication/server/module.d.ts +5 -0
  39. package/authentication/server/module.js +9 -1
  40. package/authentication/tests/authentication.api-controller.test.js +1 -1
  41. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  42. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  43. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  44. package/authentication/tests/authentication.client-service.test.js +1 -1
  45. package/browser/README.md +401 -0
  46. package/cancellation/README.md +156 -0
  47. package/cancellation/tests/coverage.test.d.ts +1 -0
  48. package/cancellation/tests/coverage.test.js +49 -0
  49. package/cancellation/tests/leak.test.js +24 -29
  50. package/cancellation/tests/token.test.d.ts +1 -0
  51. package/cancellation/tests/token.test.js +136 -0
  52. package/cancellation/token.d.ts +53 -177
  53. package/cancellation/token.js +132 -208
  54. package/circuit-breaker/postgres/module.d.ts +1 -0
  55. package/circuit-breaker/postgres/module.js +5 -1
  56. package/context/README.md +174 -0
  57. package/cookie/README.md +161 -0
  58. package/css/README.md +157 -0
  59. package/data-structures/README.md +320 -0
  60. package/decorators/README.md +140 -0
  61. package/distributed-loop/README.md +231 -0
  62. package/distributed-loop/distributed-loop.js +1 -1
  63. package/document-management/README.md +403 -0
  64. package/document-management/server/configure.js +5 -1
  65. package/document-management/server/module.d.ts +1 -1
  66. package/document-management/server/module.js +1 -1
  67. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  68. package/document-management/server/services/document-management.service.js +9 -7
  69. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  70. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  71. package/document-management/tests/document-management-core.test.js +2 -7
  72. package/document-management/tests/document-management.api.test.js +6 -7
  73. package/document-management/tests/document-statistics.service.test.js +11 -12
  74. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  75. package/document-management/tests/document.service.test.js +3 -3
  76. package/document-management/tests/enum-helpers.test.js +2 -3
  77. package/dom/README.md +213 -0
  78. package/enumerable/README.md +259 -0
  79. package/enumeration/README.md +121 -0
  80. package/errors/README.md +267 -0
  81. package/examples/document-management/main.d.ts +1 -0
  82. package/examples/document-management/main.js +14 -11
  83. package/file/README.md +191 -0
  84. package/formats/README.md +210 -0
  85. package/function/README.md +144 -0
  86. package/http/README.md +318 -0
  87. package/http/client/adapters/undici.adapter.js +1 -1
  88. package/http/client/http-client-request.d.ts +6 -5
  89. package/http/client/http-client-request.js +8 -9
  90. package/http/server/node/node-http-server.js +1 -2
  91. package/image-service/README.md +137 -0
  92. package/injector/README.md +491 -0
  93. package/intl/README.md +113 -0
  94. package/json-path/README.md +182 -0
  95. package/jsx/README.md +154 -0
  96. package/key-value-store/README.md +191 -0
  97. package/key-value-store/postgres/module.d.ts +1 -0
  98. package/key-value-store/postgres/module.js +5 -1
  99. package/lock/README.md +249 -0
  100. package/lock/postgres/module.d.ts +1 -0
  101. package/lock/postgres/module.js +5 -1
  102. package/lock/web/web-lock.js +119 -47
  103. package/logger/README.md +287 -0
  104. package/mail/README.md +256 -0
  105. package/mail/module.d.ts +5 -1
  106. package/mail/module.js +11 -6
  107. package/memory/README.md +144 -0
  108. package/message-bus/README.md +244 -0
  109. package/message-bus/message-bus-base.js +1 -1
  110. package/module/README.md +182 -0
  111. package/module/module.d.ts +1 -1
  112. package/module/module.js +77 -17
  113. package/module/modules/web-server.module.js +3 -4
  114. package/notification/server/module.d.ts +1 -0
  115. package/notification/server/module.js +5 -1
  116. package/notification/tests/notification-flow.test.js +2 -2
  117. package/notification/tests/notification-type.service.test.js +24 -15
  118. package/object-storage/README.md +300 -0
  119. package/openid-connect/README.md +274 -0
  120. package/orm/README.md +423 -0
  121. package/orm/decorators.d.ts +5 -1
  122. package/orm/decorators.js +1 -1
  123. package/orm/server/drizzle/schema-converter.js +17 -30
  124. package/orm/server/encryption.d.ts +0 -1
  125. package/orm/server/encryption.js +1 -4
  126. package/orm/server/index.d.ts +1 -6
  127. package/orm/server/index.js +1 -6
  128. package/orm/server/migration.d.ts +19 -0
  129. package/orm/server/migration.js +72 -0
  130. package/orm/server/repository.d.ts +1 -1
  131. package/orm/server/transaction.d.ts +5 -10
  132. package/orm/server/transaction.js +22 -26
  133. package/orm/server/transactional.js +3 -3
  134. package/orm/tests/database-migration.test.d.ts +1 -0
  135. package/orm/tests/database-migration.test.js +82 -0
  136. package/orm/tests/encryption.test.js +3 -4
  137. package/orm/utils.d.ts +17 -2
  138. package/orm/utils.js +49 -1
  139. package/package.json +9 -6
  140. package/password/README.md +164 -0
  141. package/pdf/README.md +246 -0
  142. package/polyfills.js +1 -0
  143. package/pool/README.md +198 -0
  144. package/process/README.md +237 -0
  145. package/promise/README.md +252 -0
  146. package/promise/cancelable-promise.js +1 -1
  147. package/random/README.md +193 -0
  148. package/rate-limit/postgres/module.d.ts +1 -0
  149. package/rate-limit/postgres/module.js +5 -1
  150. package/reflection/README.md +305 -0
  151. package/reflection/decorator-data.js +11 -12
  152. package/rpc/README.md +386 -0
  153. package/rxjs-utils/README.md +262 -0
  154. package/schema/README.md +342 -0
  155. package/serializer/README.md +342 -0
  156. package/signals/implementation/README.md +134 -0
  157. package/sse/README.md +278 -0
  158. package/task-queue/README.md +293 -0
  159. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  160. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  161. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  162. package/task-queue/postgres/module.d.ts +1 -0
  163. package/task-queue/postgres/module.js +5 -1
  164. package/task-queue/postgres/schemas.d.ts +9 -6
  165. package/task-queue/postgres/schemas.js +4 -3
  166. package/task-queue/postgres/task-queue.d.ts +4 -13
  167. package/task-queue/postgres/task-queue.js +462 -355
  168. package/task-queue/postgres/task.model.d.ts +12 -5
  169. package/task-queue/postgres/task.model.js +51 -25
  170. package/task-queue/task-context.d.ts +2 -2
  171. package/task-queue/task-context.js +8 -8
  172. package/task-queue/task-queue.d.ts +53 -19
  173. package/task-queue/task-queue.js +121 -55
  174. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  175. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  176. package/task-queue/tests/complex.test.js +45 -229
  177. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  178. package/task-queue/tests/coverage-branch.test.js +407 -0
  179. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  180. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  181. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  182. package/task-queue/tests/dag-dependencies.test.js +41 -0
  183. package/task-queue/tests/dependencies.test.js +28 -26
  184. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  185. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  186. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  187. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  188. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  189. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  190. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  191. package/task-queue/tests/queue.test.js +128 -8
  192. package/task-queue/tests/worker.test.js +39 -16
  193. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  194. package/task-queue/tests/zombie-parent.test.js +45 -0
  195. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  196. package/task-queue/tests/zombie-recovery.test.js +51 -0
  197. package/templates/README.md +287 -0
  198. package/test5.js +5 -5
  199. package/testing/README.md +157 -0
  200. package/testing/integration-setup.d.ts +4 -4
  201. package/testing/integration-setup.js +54 -29
  202. package/text/README.md +346 -0
  203. package/text/localization.service.js +2 -2
  204. package/threading/README.md +238 -0
  205. package/types/README.md +311 -0
  206. package/utils/README.md +322 -0
  207. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  208. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  209. package/utils/async-iterable-helpers/take-until.js +4 -4
  210. package/utils/backoff.js +89 -30
  211. package/utils/file-reader.js +1 -2
  212. package/utils/retry-with-backoff.js +1 -1
  213. package/utils/timer.d.ts +1 -1
  214. package/utils/timer.js +5 -7
  215. package/utils/timing.d.ts +1 -1
  216. package/utils/timing.js +2 -4
  217. package/utils/z-base32.d.ts +1 -0
  218. 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
- // Use the QueueProvider to Create/Retrieve the queue.
18
- // This is the cleanest way to get a configured queue instance without
19
- // messing with `injectArgument` manually in the test body.
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
- await queue.clear();
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 AND new ID
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, 'New ID if replaced').not.toBe(t1.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?.visibilityDeadline > initialLock).toBe(true);
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 === 2)
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
- await timeout(50);
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(500);
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 === tFail.id) {
94
+ if (context.id == tFail.id) {
87
95
  return TaskProcessResult.Fail(new Error('explicit fail'));
88
96
  }
89
- if (context.id === tResched.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
- if (processed.size === 2)
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
- const otherChildren = await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
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
- for (let i = 0; i < 50; i++) {
133
- if (executed)
134
- break;
135
- await timeout(20);
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 === 1) {
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 === 2)
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
+ });