@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,6 +1,6 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
2
  import { CancellationToken } from '../../cancellation/index.js';
3
- import { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
4
4
  import { setupIntegrationTest } from '../../testing/index.js';
5
5
  import { timeout } from '../../utils/timing.js';
6
6
  describe('Queue Dependencies & Tree Tests', () => {
@@ -28,35 +28,38 @@ describe('Queue Dependencies & Tree Tests', () => {
28
28
  if (task?.status == status) {
29
29
  return;
30
30
  }
31
- await queue.processPendingFanIn();
32
31
  await timeout(50);
33
32
  }
34
33
  }
35
- describe('Dependencies (Fan-In)', () => {
36
- it('should schedule a task only after dependency completes (completeAfterTags)', async () => {
37
- // 1. Create a dependent task (Waiting)
34
+ describe('Dependencies (DAG)', () => {
35
+ it('should schedule a task only after dependency completes (completeAfter)', async () => {
36
+ // 1. Prereq
37
+ const prereq = await queue.enqueue('prereq', { val: 1 });
38
+ // 2. Dependent (Waiting)
38
39
  const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
39
- completeAfterTags: ['tag-a'],
40
+ completeAfter: [prereq.id],
40
41
  });
41
- expect(dependent.status).toBe(TaskStatus.Waiting);
42
- // 2. Create the prerequisite task
43
- const prereq = await queue.enqueue('prereq', { val: 1 }, { tags: ['tag-a'] });
42
+ expect(dependent.status).toBe(TaskStatus.Pending);
44
43
  // 3. Complete prereq
45
44
  const dequeued = await queue.dequeue({ types: ['prereq'] });
46
45
  expect(dequeued?.id).toBe(prereq.id);
47
46
  await queue.complete(dequeued);
48
- await waitForStatus(dependent.id, TaskStatus.Completed);
47
+ // Complete dependent too so it reaches finalized state for waitForTasks
48
+ const dDependent = await queue.dequeue({ types: ['dependent'] });
49
+ expect(dDependent?.id).toBe(dependent.id);
50
+ await queue.complete(dDependent);
51
+ await queue.waitForTasks([dependent.id]);
49
52
  const updatedDependent = await queue.getTask(dependent.id);
50
53
  expect(updatedDependent?.status).toBe(TaskStatus.Completed);
51
54
  });
52
- it('should schedule a task to run after dependency completes (scheduleAfterTags)', async () => {
53
- // 1. Dependent task
55
+ it('should schedule a task to run after dependency completes (scheduleAfter)', async () => {
56
+ // 1. Prereq
57
+ const prereq = await queue.enqueue('prereq', {});
58
+ // 2. Dependent task
54
59
  const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
55
- scheduleAfterTags: ['tag-b'],
60
+ scheduleAfter: [prereq.id],
56
61
  });
57
62
  expect(dependent.status).toBe(TaskStatus.Waiting);
58
- // 2. Prereq
59
- const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-b'] });
60
63
  // 3. Complete prereq
61
64
  const dequeued = await queue.dequeue({ types: ['prereq'] });
62
65
  await queue.complete(dequeued);
@@ -69,29 +72,28 @@ describe('Queue Dependencies & Tree Tests', () => {
69
72
  expect(d2?.id).toBe(dependent.id);
70
73
  });
71
74
  it('should fail-fast if dependency fails', async () => {
75
+ const prereq = await queue.enqueue('prereq', {});
72
76
  const dependent = await queue.enqueue('dependent', {}, {
73
- scheduleAfterTags: ['tag-fail'],
77
+ scheduleAfter: [prereq.id],
74
78
  failFast: true
75
79
  });
76
- const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-fail'] });
77
80
  const dequeued = await queue.dequeue({ types: ['prereq'] });
78
81
  // Fail fatally
79
82
  await queue.fail(dequeued, new Error('boom'), { fatal: true });
80
- await waitForStatus(dependent.id, TaskStatus.Dead);
83
+ await queue.waitForTasks([dependent.id]);
81
84
  const updatedDependent = await queue.getTask(dependent.id);
82
85
  expect(updatedDependent?.status).toBe(TaskStatus.Dead);
83
86
  expect(updatedDependent?.error?.code).toBe('DependencyFailed');
84
87
  });
85
- it('should respect DependencyJoinMode.Or', async () => {
86
- // Wait for tag-1 OR tag-2
88
+ it('should respect requiredStatuses', async () => {
89
+ const prereq = await queue.enqueue('prereq', {});
90
+ // Wait for prereq to be Dead or Completed
87
91
  const dependent = await queue.enqueue('dependent', {}, {
88
- scheduleAfterTags: ['tag-1', 'tag-2'],
89
- dependencyJoinMode: DependencyJoinMode.Or
92
+ scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }]
90
93
  });
91
- // Complete tag-1 only
92
- await queue.enqueue('t1', {}, { tags: ['tag-1'] });
93
- const d1 = await queue.dequeue({ types: ['t1'] });
94
- await queue.complete(d1);
94
+ // Fail fatally -> Dead
95
+ const d1 = await queue.dequeue({ types: ['prereq'] });
96
+ await queue.fail(d1, new Error('boom'), { fatal: true });
95
97
  await waitForStatus(dependent.id, TaskStatus.Pending);
96
98
  const updated = await queue.getTask(dependent.id);
97
99
  expect(updated?.status).toBe(TaskStatus.Pending);
@@ -1,5 +1,5 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
- import { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
2
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
3
  import { setupIntegrationTest } from '../../testing/index.js';
4
4
  import { timeout } from '../../utils/timing.js';
5
5
  describe('Extensive Task Queue Dependency Tests', () => {
@@ -26,7 +26,6 @@ describe('Extensive Task Queue Dependency Tests', () => {
26
26
  const task = await queue.getTask(id);
27
27
  if (task?.status === status)
28
28
  return;
29
- await queue.processPendingFanIn();
30
29
  await timeout(50);
31
30
  }
32
31
  const finalTask = await queue.getTask(id);
@@ -37,19 +36,18 @@ describe('Extensive Task Queue Dependency Tests', () => {
37
36
  if (!dequeued)
38
37
  throw new Error(`Could not dequeue task of type ${type}`);
39
38
  await queue.complete(dequeued);
40
- await queue.processPendingFanIn();
41
39
  }
42
40
  it('should handle complex mixed chain: A -> (B -> D, C -> E) -> F', async () => {
43
- // F depends on D and E
44
- const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['mc-d', 'mc-e'] });
45
- // D depends on B, E depends on C
46
- const taskD = await queue.enqueue('D', {}, { tags: ['mc-d'], scheduleAfterTags: ['mc-b'] });
47
- const taskE = await queue.enqueue('E', {}, { tags: ['mc-e'], scheduleAfterTags: ['mc-c'] });
48
- // B and C depend on A
49
- const taskB = await queue.enqueue('B', {}, { tags: ['mc-b'], scheduleAfterTags: ['mc-a'] });
50
- const taskC = await queue.enqueue('C', {}, { tags: ['mc-c'], scheduleAfterTags: ['mc-a'] });
51
41
  // A is the root
52
- const taskA = await queue.enqueue('A', {}, { tags: ['mc-a'] });
42
+ const taskA = await queue.enqueue('A', {});
43
+ // B and C depend on A
44
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
45
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
46
+ // D depends on B, E depends on C
47
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id] });
48
+ const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id] });
49
+ // F depends on D and E
50
+ const taskF = await queue.enqueue('F', {}, { scheduleAfter: [taskD.id, taskE.id] });
53
51
  expect(taskA.status).toBe(TaskStatus.Pending);
54
52
  expect(taskB.status).toBe(TaskStatus.Waiting);
55
53
  expect(taskC.status).toBe(TaskStatus.Waiting);
@@ -77,158 +75,85 @@ describe('Extensive Task Queue Dependency Tests', () => {
77
75
  await completeTask('E');
78
76
  await waitForStatus(taskF.id, TaskStatus.Pending);
79
77
  });
80
- it('should handle requested pattern: A -> B & C -> D', async () => {
81
- const taskD = await queue.enqueue('D', {}, { scheduleAfterTags: ['B', 'C'] });
82
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
83
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A'] });
84
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
85
- // Initial check
86
- expect(taskA.status).toBe(TaskStatus.Pending);
87
- expect(taskB.status).toBe(TaskStatus.Waiting);
88
- expect(taskC.status).toBe(TaskStatus.Waiting);
89
- expect(taskD.status).toBe(TaskStatus.Waiting);
90
- // Complete A
78
+ it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
79
+ const taskA = await queue.enqueue('A', {});
80
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
81
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
82
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
91
83
  await completeTask('A');
92
84
  await waitForStatus(taskB.id, TaskStatus.Pending);
93
85
  await waitForStatus(taskC.id, TaskStatus.Pending);
94
- // B and C can be dequeued now (in any order)
95
- const dB = await queue.dequeue({ types: ['B'] });
96
- const dC = await queue.dequeue({ types: ['C'] });
97
- expect(dB).toBeDefined();
98
- expect(dC).toBeDefined();
99
- // D still waiting
100
- expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
101
- // Complete B
102
- await queue.complete(dB);
103
- await queue.processPendingFanIn();
86
+ await completeTask('B');
104
87
  expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
105
- // Complete C
106
- await queue.complete(dC);
88
+ await completeTask('C');
107
89
  await waitForStatus(taskD.id, TaskStatus.Pending);
108
90
  });
109
- it('should strictly adhere to order: A -> B -> C', async () => {
110
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'] });
111
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
112
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
113
- // Try to dequeue B and C - should fail
114
- const dC = await queue.dequeue({ types: ['C'] });
115
- const dB = await queue.dequeue({ types: ['B'] });
116
- expect(dC).toBeUndefined();
117
- expect(dB).toBeUndefined();
118
- // Complete A
119
- const dA = await queue.dequeue({ types: ['A'] });
120
- expect(dA?.id).toBe(taskA.id);
121
- await queue.complete(dA);
122
- await queue.processPendingFanIn();
123
- // Now B should be available, but C still not
124
- const dC_2 = await queue.dequeue({ types: ['C'] });
125
- expect(dC_2).toBeUndefined();
126
- const dB_2 = await queue.dequeue({ types: ['B'] });
127
- expect(dB_2?.id).toBe(taskB.id);
128
- await queue.complete(dB_2);
129
- await queue.processPendingFanIn();
130
- // Now C should be available
131
- const dC_3 = await queue.dequeue({ types: ['C'] });
132
- expect(dC_3?.id).toBe(taskC.id);
133
- await queue.complete(dC_3);
134
- });
135
- it('should handle large fan-in: (T1, T2, T3, T4, T5) -> Result', async () => {
136
- const tags = ['t1', 't2', 't3', 't4', 't5'];
137
- const taskResult = await queue.enqueue('Result', {}, { scheduleAfterTags: tags });
138
- for (const tag of tags) {
139
- await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
140
- }
141
- // Complete all but one
142
- for (let i = 0; i < 4; i++) {
143
- await completeTask(`Task-${tags[i]}`);
144
- expect((await queue.getTask(taskResult.id))?.status).toBe(TaskStatus.Waiting);
145
- }
146
- // Complete the last one
147
- await completeTask(`Task-${tags[4]}`);
148
- await waitForStatus(taskResult.id, TaskStatus.Pending);
91
+ it('should handle Deep Chain (A -> B -> C -> D)', async () => {
92
+ const taskA = await queue.enqueue('A', {});
93
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
94
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
95
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
96
+ await completeTask('A');
97
+ await waitForStatus(taskB.id, TaskStatus.Pending);
98
+ await completeTask('B');
99
+ await waitForStatus(taskC.id, TaskStatus.Pending);
100
+ await completeTask('C');
101
+ await waitForStatus(taskD.id, TaskStatus.Pending);
149
102
  });
150
- it('should handle large fan-in with OR: (T1, T2, T3, T4, T5) -> Result', async () => {
151
- const tags = ['o1', 'o2', 'o3', 'o4', 'o5'];
152
- const taskResult = await queue.enqueue('Result', {}, {
153
- scheduleAfterTags: tags,
154
- dependencyJoinMode: DependencyJoinMode.Or
155
- });
156
- for (const tag of tags) {
157
- await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
103
+ it('should handle wide fan-out (A -> [B1...B50] -> C)', async () => {
104
+ const taskA = await queue.enqueue('A', {});
105
+ const bTasks = await queue.enqueueMany(Array.from({ length: 50 }, (_, i) => ({ type: `B${i}`, data: {}, scheduleAfter: [taskA.id] })), { returnTasks: true });
106
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: bTasks.map(t => t.id) });
107
+ await completeTask('A');
108
+ for (let i = 0; i < 49; i++) {
109
+ await completeTask(`B${i}`);
110
+ expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
158
111
  }
159
- // Complete one
160
- await completeTask(`Task-${tags[2]}`);
161
- await waitForStatus(taskResult.id, TaskStatus.Pending);
112
+ await completeTask('B49');
113
+ await waitForStatus(taskC.id, TaskStatus.Pending);
162
114
  });
163
- it('should handle Diamond of Diamonds: A -> (B1, B2) -> C -> (D1, D2) -> E', async () => {
164
- const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['tag-d1', 'tag-d2'] });
165
- const taskD1 = await queue.enqueue('D1', {}, { tags: ['tag-d1'], scheduleAfterTags: ['tag-c'] });
166
- const taskD2 = await queue.enqueue('D2', {}, { tags: ['tag-d2'], scheduleAfterTags: ['tag-c'] });
167
- const taskC = await queue.enqueue('C', {}, { tags: ['tag-c'], scheduleAfterTags: ['tag-b1', 'tag-b2'] });
168
- const taskB1 = await queue.enqueue('B1', {}, { tags: ['tag-b1'], scheduleAfterTags: ['tag-a'] });
169
- const taskB2 = await queue.enqueue('B2', {}, { tags: ['tag-b2'], scheduleAfterTags: ['tag-a'] });
170
- const taskA = await queue.enqueue('A', {}, { tags: ['tag-a'] });
171
- // Step by step completion
115
+ it('should handle deep tree with branching and merging', async () => {
116
+ const taskA = await queue.enqueue('A', {});
117
+ const taskB1 = await queue.enqueue('B1', {}, { scheduleAfter: [taskA.id] });
118
+ const taskB2 = await queue.enqueue('B2', {}, { scheduleAfter: [taskA.id] });
119
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB1.id, taskB2.id] });
120
+ const taskD1 = await queue.enqueue('D1', {}, { scheduleAfter: [taskC.id] });
121
+ const taskD2 = await queue.enqueue('D2', {}, { scheduleAfter: [taskC.id] });
122
+ const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskD1.id, taskD2.id] });
172
123
  await completeTask('A');
173
- await waitForStatus(taskB1.id, TaskStatus.Pending);
174
- await waitForStatus(taskB2.id, TaskStatus.Pending);
175
124
  await completeTask('B1');
176
- expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
177
125
  await completeTask('B2');
178
126
  await waitForStatus(taskC.id, TaskStatus.Pending);
179
127
  await completeTask('C');
180
128
  await waitForStatus(taskD1.id, TaskStatus.Pending);
181
129
  await waitForStatus(taskD2.id, TaskStatus.Pending);
182
130
  await completeTask('D1');
183
- expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
184
131
  await completeTask('D2');
185
132
  await waitForStatus(taskE.id, TaskStatus.Pending);
186
133
  });
187
- it('should fail-fast entire branch if one dependency fails fatal', async () => {
188
- // A -> B -> C
189
- // -> D -> E
190
- // (C & E) -> F
191
- const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'E'], failFast: true });
192
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'], failFast: true });
193
- const taskE = await queue.enqueue('E', {}, { tags: ['E'], scheduleAfterTags: ['D'], failFast: true });
194
- const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'], failFast: true });
195
- const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A'], failFast: true });
196
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
197
- await completeTask('A');
198
- await waitForStatus(taskB.id, TaskStatus.Pending);
199
- await waitForStatus(taskD.id, TaskStatus.Pending);
200
- // Fail B fatally
201
- const dB = await queue.dequeue({ types: ['B'] });
202
- await queue.fail(dB, new Error('fatal B'), { fatal: true });
203
- await queue.processPendingFanIn();
204
- // B failed fatally -> C should die -> F should die
134
+ it('should handle fail-fast cascade', async () => {
135
+ const taskA = await queue.enqueue('A', {});
136
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: true });
137
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id], failFast: true });
138
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskA.id], failFast: true });
139
+ const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id], failFast: true });
140
+ const dA = await queue.dequeue({ types: ['A'] });
141
+ await queue.fail(dA, new Error('fatal'), { fatal: true });
142
+ await waitForStatus(taskB.id, TaskStatus.Dead);
205
143
  await waitForStatus(taskC.id, TaskStatus.Dead);
206
- await waitForStatus(taskF.id, TaskStatus.Dead);
207
- // D and E should be unaffected (except E is still waiting for D)
208
- const uD = await queue.getTask(taskD.id);
209
- expect(uD?.status).toBe(TaskStatus.Pending);
144
+ await waitForStatus(taskD.id, TaskStatus.Dead);
145
+ await waitForStatus(taskE.id, TaskStatus.Dead);
210
146
  });
211
- it('should handle many-to-many dependencies', async () => {
212
- // {A, B} -> {C, D} -> {E, F}
213
- // Each of C, D depends on BOTH A and B.
214
- // Each of E, F depends on BOTH C and D.
215
- const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['C', 'D'] });
216
- const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'D'] });
217
- const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A', 'B'] });
218
- const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A', 'B'] });
219
- const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
220
- const taskB = await queue.enqueue('B', {}, { tags: ['B'] });
147
+ it('should NOT schedule if only one of two dependencies is met (AND logic)', async () => {
148
+ const taskA = await queue.enqueue('A', {});
149
+ const taskB = await queue.enqueue('B', {});
150
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id, taskB.id] });
221
151
  await completeTask('A');
222
152
  expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
223
- expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
153
+ // Still waiting even after some time
154
+ await timeout(100);
155
+ expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
224
156
  await completeTask('B');
225
157
  await waitForStatus(taskC.id, TaskStatus.Pending);
226
- await waitForStatus(taskD.id, TaskStatus.Pending);
227
- await completeTask('C');
228
- expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
229
- expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
230
- await completeTask('D');
231
- await waitForStatus(taskE.id, TaskStatus.Pending);
232
- await waitForStatus(taskF.id, TaskStatus.Pending);
233
158
  });
234
159
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
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('Fan-Out Spawning', () => {
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 = `fan-out-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 transition parent to Waiting when spawning children during execution', async () => {
25
+ const parent = await queue.enqueue('parent', {});
26
+ const dParent = await queue.dequeue();
27
+ expect(dParent?.id).toBe(parent.id);
28
+ // Parent spawns a child
29
+ const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id }], { returnTasks: true });
30
+ // Complete parent
31
+ await queue.complete(dParent);
32
+ // Parent should be Waiting for child
33
+ const uParent = await queue.getTask(parent.id);
34
+ expect(uParent?.status).toBe(TaskStatus.WaitingChildren);
35
+ expect(uParent?.unresolvedCompleteDependencies).toBe(1);
36
+ // Complete child
37
+ const dChild = await queue.dequeue();
38
+ await queue.complete(dChild);
39
+ // Parent should now be Completed
40
+ const fParent = await queue.getTask(parent.id);
41
+ expect(fParent?.status).toBe(TaskStatus.Completed);
42
+ });
43
+ it('should NOT transition parent to Waiting if waitForCompletion is false', async () => {
44
+ const parent = await queue.enqueue('parent', {});
45
+ const dParent = await queue.dequeue();
46
+ // Spawn child with waitForCompletion: false
47
+ await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, waitForCompletion: false }], { returnTasks: true });
48
+ await queue.complete(dParent);
49
+ const uParent = await queue.getTask(parent.id);
50
+ expect(uParent?.status).toBe(TaskStatus.Completed); // Finished immediately
51
+ expect(uParent?.unresolvedCompleteDependencies).toBe(0);
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
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('Idempotent Replacement', () => {
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 = `idempotent-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 NOT be affected by "ghost" dependencies after replacement', async () => {
25
+ const depA = await queue.enqueue('depA', {});
26
+ const depB = await queue.enqueue('depB', {});
27
+ const depC = await queue.enqueue('depC', {});
28
+ const idempotencyKey = 'my-idempotent-task';
29
+ // 1. Initial enqueue with A and B
30
+ const taskV1 = await queue.enqueue('main', { version: 1 }, {
31
+ idempotencyKey,
32
+ scheduleAfter: [depA.id, depB.id],
33
+ });
34
+ expect(taskV1.status).toBe(TaskStatus.Waiting);
35
+ expect(taskV1.unresolvedScheduleDependencies).toBe(2);
36
+ // 2. Replacement enqueue with only C
37
+ const taskV2 = await queue.enqueue('main', { version: 2 }, {
38
+ idempotencyKey,
39
+ scheduleAfter: [depC.id],
40
+ replace: true,
41
+ });
42
+ expect(taskV2.id).toBe(taskV1.id);
43
+ expect(taskV2.unresolvedScheduleDependencies).toBe(1);
44
+ expect(taskV2.data).toEqual({ version: 2 });
45
+ // 3. Complete an OLD dependency (depA)
46
+ const dDepA = await queue.dequeue({ types: ['depA'] });
47
+ await queue.complete(dDepA);
48
+ // Give it a moment to process background dependency resolution if any
49
+ await timeout(100);
50
+ // 4. Verify main task is STILL waiting for depC
51
+ const uTask = await queue.getTask(taskV1.id);
52
+ expect(uTask?.status).toBe(TaskStatus.Waiting);
53
+ expect(uTask?.unresolvedScheduleDependencies).toBe(1);
54
+ // 5. Complete NEW dependency (depC)
55
+ const dDepC = await queue.dequeue({ types: ['depC'] });
56
+ await queue.complete(dDepC);
57
+ // 6. Verify main task is now Pending
58
+ const fTask = await queue.getTask(taskV1.id);
59
+ expect(fTask?.status).toBe(TaskStatus.Pending);
60
+ });
61
+ });
@@ -0,0 +1,38 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { TaskQueueProvider } from '../../task-queue/index.js';
3
+ import { setupIntegrationTest } from '../../testing/index.js';
4
+ describe('Missing Idempotent Tasks Bug', () => {
5
+ let injector;
6
+ let queue;
7
+ beforeAll(async () => {
8
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
9
+ });
10
+ beforeEach(() => {
11
+ const queueProvider = injector.resolve(TaskQueueProvider);
12
+ const queueName = `missing-idem-queue-${Date.now()}-${Math.random()}`;
13
+ queue = queueProvider.get(queueName, {
14
+ visibilityTimeout: 1000,
15
+ });
16
+ });
17
+ afterEach(async () => {
18
+ await queue.clear();
19
+ });
20
+ afterAll(async () => {
21
+ await injector?.dispose();
22
+ });
23
+ it('should NOT crash when enqueuing duplicate idempotent tasks with dependencies and returnTasks: false', async () => {
24
+ const key = 'idem-key';
25
+ const dep = await queue.enqueue('dep', {});
26
+ // 1. Initial enqueue
27
+ await queue.enqueue('main', {}, { idempotencyKey: key });
28
+ // 2. Second enqueue with same key and a dependency, returnTasks: false
29
+ // This should NOT crash.
30
+ // It should ideally either:
31
+ // a) Ignore the new dependency because it's a duplicate and replace=false
32
+ // b) Add the dependency to the existing task (though replace=false usually means ignore)
33
+ // The current bug is that it CRASHES with TypeError.
34
+ await expect(queue.enqueueMany([
35
+ { type: 'main', data: {}, idempotencyKey: key, scheduleAfter: [dep.id] }
36
+ ], { replace: false, returnTasks: false })).resolves.toBeUndefined();
37
+ });
38
+ });