@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
@@ -0,0 +1,407 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { CancellationToken } from '../../cancellation/index.js';
3
+ import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
4
+ import { runInInjectionContext } from '../../injector/index.js';
5
+ import { injectRepository } from '../../orm/server/index.js';
6
+ import { TaskProcessResult, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
7
+ import { PostgresTask } from '../../task-queue/postgres/index.js';
8
+ import { setupIntegrationTest } from '../../testing/index.js';
9
+ import { currentTimestamp } from '../../utils/date-time.js';
10
+ import { timeout } from '../../utils/timing.js';
11
+ describe('Task Queue Branch Coverage Enhancement', () => {
12
+ let injector;
13
+ let queue;
14
+ let token;
15
+ beforeAll(async () => {
16
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
17
+ });
18
+ beforeEach(() => {
19
+ const queueProvider = injector.resolve(TaskQueueProvider);
20
+ const queueName = `branch-coverage-queue-${Date.now()}-${Math.random()}`;
21
+ queue = queueProvider.get(queueName, {
22
+ visibilityTimeout: 200,
23
+ });
24
+ token = new CancellationToken();
25
+ });
26
+ afterEach(async () => {
27
+ token.set();
28
+ await queue.clear();
29
+ });
30
+ afterAll(async () => {
31
+ await injector?.dispose();
32
+ });
33
+ it('should handle TaskProcessResult.RescheduleTo in worker', async () => {
34
+ const task = await queue.enqueue('reschedule-test', {});
35
+ const future = currentTimestamp() + 5000;
36
+ queue.process({ cancellationSignal: token }, async () => {
37
+ return TaskProcessResult.RescheduleTo(future);
38
+ });
39
+ await timeout(500);
40
+ token.set();
41
+ const updated = await queue.getTask(task.id);
42
+ expect(updated?.status).toBe(TaskStatus.Pending);
43
+ expect(updated?.scheduleTimestamp).toBe(future);
44
+ });
45
+ it('should handle TaskProcessResult.Fail(fatal: true) in worker', async () => {
46
+ const task = await queue.enqueue('fail-fatal-test', {});
47
+ queue.process({ cancellationSignal: token }, async () => {
48
+ return TaskProcessResult.Fail(new Error('fatal error'), true);
49
+ });
50
+ await timeout(500);
51
+ token.set();
52
+ const updated = await queue.getTask(task.id);
53
+ expect(updated?.status).toBe(TaskStatus.Dead);
54
+ });
55
+ it('should handle errors in touch during background lease extension', async () => {
56
+ const task = await queue.enqueue('touch-error-test', {});
57
+ // Mock touch to throw once
58
+ const originalTouch = queue.touch.bind(queue);
59
+ let thrown = false;
60
+ vi.spyOn(queue, 'touch').mockImplementation(async (t, o) => {
61
+ if (!thrown) {
62
+ thrown = true;
63
+ throw new Error('touch error');
64
+ }
65
+ return originalTouch(t, o);
66
+ });
67
+ queue.process({ cancellationSignal: token }, async () => {
68
+ await timeout(300); // Wait for background touch to trigger
69
+ return TaskProcessResult.Complete();
70
+ });
71
+ await timeout(600);
72
+ token.set();
73
+ const updated = await queue.getTask(task.id);
74
+ expect(updated?.status).toBe(TaskStatus.Completed);
75
+ expect(thrown).toBe(true);
76
+ });
77
+ it('should handle background lease loss during processing', async () => {
78
+ // visibilityTimeout is 200ms. Background touch is every 100ms.
79
+ const task = await queue.enqueue('lease-loss-test', {});
80
+ let workerFinished = false;
81
+ // Mock touch to return undefined (stolen/lost)
82
+ vi.spyOn(queue, 'touch').mockResolvedValue(undefined);
83
+ queue.process({ cancellationSignal: token }, async (context) => {
84
+ // Wait for background touch to discover lease loss
85
+ for (let i = 0; i < 20; i++) {
86
+ if (context.signal.isSet)
87
+ break;
88
+ await timeout(50);
89
+ }
90
+ workerFinished = true;
91
+ return TaskProcessResult.Complete();
92
+ });
93
+ await timeout(500);
94
+ token.set();
95
+ expect(workerFinished).toBe(true);
96
+ });
97
+ it('should handle enqueueMany with empty items', async () => {
98
+ const result = await queue.enqueueMany([], { returnTasks: true });
99
+ expect(result).toEqual([]);
100
+ const result2 = await queue.enqueueMany([]);
101
+ expect(result2).toBeUndefined();
102
+ });
103
+ it('should handle getTask with non-existent id', async () => {
104
+ const task = await queue.getTask('00000000-0000-0000-0000-000000000000');
105
+ expect(task).toBeUndefined();
106
+ });
107
+ it('should handle dequeue with specific types', async () => {
108
+ await queue.enqueue('type1', { val: 1 });
109
+ await queue.enqueue('type2', { val: 2 });
110
+ const d1 = await queue.dequeue({ types: ['type2'] });
111
+ expect(d1?.type).toBe('type2');
112
+ const d2 = await queue.dequeue({ types: ['type1'] });
113
+ expect(d2?.type).toBe('type1');
114
+ });
115
+ it('should handle maintenance when no tasks need maintenance', async () => {
116
+ await expect(queue.maintenance()).resolves.toBeUndefined();
117
+ });
118
+ it('should handle multiple workers competing for tasks', async () => {
119
+ await queue.enqueue('compete', {});
120
+ // Dequeue in one worker
121
+ const d1 = await queue.dequeue();
122
+ expect(d1).toBeDefined();
123
+ // Try to dequeue again - should be empty because it is locked
124
+ const d2 = await queue.dequeue();
125
+ expect(d2).toBeUndefined();
126
+ });
127
+ it('should handle reschedule by milliseconds', async () => {
128
+ const task = await queue.enqueue('reschedule-by', {});
129
+ const dequeued = await queue.dequeue();
130
+ const future = currentTimestamp() + 5000;
131
+ await queue.reschedule(dequeued.id, future);
132
+ const updated = await queue.getTask(task.id);
133
+ expect(updated?.scheduleTimestamp).toBe(future);
134
+ });
135
+ it('should handle touch with no tasks', async () => {
136
+ const result = await queue.touchMany([]);
137
+ expect(result).toEqual([]);
138
+ });
139
+ it('should handle touch with no task token', async () => {
140
+ const task = await queue.enqueue('no-token', {});
141
+ // Task is Pending, token is null
142
+ const result = await queue.touch(task);
143
+ expect(result).toBeUndefined();
144
+ });
145
+ it('should handle complete with non-existent task', async () => {
146
+ const fakeTask = { id: '00000000-0000-0000-0000-000000000000', token: '00000000-0000-0000-0000-000000000000' };
147
+ // Should not throw
148
+ await queue.complete(fakeTask);
149
+ });
150
+ it('should handle dequeueMany with forceDequeue', async () => {
151
+ await queue.enqueue('force', {});
152
+ const tasks = await queue.dequeueMany(1, { forceDequeue: true });
153
+ expect(tasks).toHaveLength(1);
154
+ });
155
+ it('should handle dequeueMany with globalConcurrency limit', async () => {
156
+ const limitedQueue = injector.resolve(TaskQueueProvider).get(`limited-${Date.now()}`, { globalConcurrency: 1 });
157
+ await limitedQueue.enqueue('t1', {});
158
+ await limitedQueue.enqueue('t2', {});
159
+ const d1 = await limitedQueue.dequeue();
160
+ expect(d1).toBeDefined();
161
+ const d2 = await limitedQueue.dequeue();
162
+ expect(d2).toBeUndefined(); // Limited by globalConcurrency
163
+ });
164
+ it('should handle waitForTasks timeout', async () => {
165
+ const task = await queue.enqueue('wait-timeout', {});
166
+ await expect(queue.waitForTasks([task.id], { timeout: 100, interval: 50 })).rejects.toThrow('Timeout');
167
+ });
168
+ it('should handle cancelMany with empty ids', async () => {
169
+ // Should not throw
170
+ await queue.cancelMany([]);
171
+ });
172
+ it('should handle touch with progress and state', async () => {
173
+ const task = await queue.enqueue('touch-data', {});
174
+ const dequeued = await queue.dequeue();
175
+ await queue.touch(dequeued, { progress: 0.7, state: { ok: true } });
176
+ const updated = await queue.getTask(task.id);
177
+ expect(updated?.progress).toBe(0.7);
178
+ expect(updated?.state).toEqual({ ok: true });
179
+ });
180
+ it('should handle touchMany with multiple tasks', async () => {
181
+ const t1 = await queue.enqueue('tm1', {});
182
+ const t2 = await queue.enqueue('tm2', {});
183
+ const d1 = await queue.dequeue();
184
+ const d2 = await queue.dequeue();
185
+ const result = await queue.touchMany([d1, d2], { progresses: [0.1, 0.2] });
186
+ expect(result).toHaveLength(2);
187
+ });
188
+ it('should handle enqueue with only completeAfter (isWaiting=false in DB, but has dependencies)', async () => {
189
+ // line 116 in pg task-queue: const isWaiting = scheduleAfterCount > 0;
190
+ // status: isWaiting ? TaskStatus.Waiting : TaskStatus.Pending
191
+ const prereq = await queue.enqueue('p', {});
192
+ const dependent = await queue.enqueue('d', {}, { completeAfter: [prereq.id] });
193
+ expect(dependent.status).toBe(TaskStatus.Pending); // Because scheduleAfterCount is 0
194
+ expect(dependent.unresolvedCompleteDependencies).toBe(1);
195
+ });
196
+ it('should handle maintenance recovering zombie tasks', async () => {
197
+ const isolatedQueue = injector.resolve(TaskQueueProvider).get(`zombie-${Date.now()}`, { visibilityTimeout: 100 });
198
+ await isolatedQueue.enqueue('z', {});
199
+ const d1 = await isolatedQueue.dequeue();
200
+ expect(d1).toBeDefined();
201
+ // Wait for visibility timeout to expire
202
+ await timeout(200);
203
+ await isolatedQueue.maintenance();
204
+ const updated = await isolatedQueue.getTask(d1.id);
205
+ expect(updated?.status).toBe(TaskStatus.Pending);
206
+ expect(updated?.token).toBeNull();
207
+ });
208
+ it('should handle getTask including archived tasks', async () => {
209
+ const archQueue = injector.resolve(TaskQueueProvider).get(`arch-${Date.now()}`, { retention: 0, archiveRetention: 3600000 });
210
+ const t = await archQueue.enqueue('t', {});
211
+ const d = await archQueue.dequeue();
212
+ await archQueue.complete(d);
213
+ await timeout(50);
214
+ await archQueue.maintenance(); // Should archive
215
+ const archived = await archQueue.getTask(t.id);
216
+ expect(archived).toBeDefined();
217
+ expect(archived?.id).toBe(t.id);
218
+ });
219
+ it('should handle maintenance with no work to do', async () => {
220
+ await queue.maintenance(); // Should not throw
221
+ });
222
+ it('should handle task TTL failure in maintenance', async () => {
223
+ // retention=0 to allow maintenance to process immediately
224
+ const ttlQueue = injector.resolve(TaskQueueProvider).get(`ttl-${Date.now()}`, { retention: 0 });
225
+ // Enqueue with TTL in the past
226
+ const task = await ttlQueue.enqueue('ttl-fail', {}, { timeToLive: currentTimestamp() - 1000 });
227
+ await timeout(100);
228
+ await ttlQueue.maintenance();
229
+ const updated = await ttlQueue.getTask(task.id);
230
+ expect(updated?.status).toBe(TaskStatus.Dead);
231
+ expect(updated?.error?.message).toContain('Task expired');
232
+ });
233
+ it('should handle cancelMany with multiple valid IDs', async () => {
234
+ const t1 = await queue.enqueue('c1', {});
235
+ const t2 = await queue.enqueue('c2', {});
236
+ await queue.cancelMany([t1.id, t2.id]);
237
+ const u1 = await queue.getTask(t1.id);
238
+ const u2 = await queue.getTask(t2.id);
239
+ expect(u1?.status).toBe(TaskStatus.Cancelled);
240
+ expect(u2?.status).toBe(TaskStatus.Cancelled);
241
+ });
242
+ it('should handle pruning long expired tasks', async () => {
243
+ // archiveRetention = 0
244
+ const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-long-${Date.now()}`, { retention: 0, archiveRetention: 0 });
245
+ const task = await pruneQueue.enqueue('p1', {});
246
+ const d1 = await pruneQueue.dequeue();
247
+ await pruneQueue.complete(d1);
248
+ await timeout(100);
249
+ await pruneQueue.maintenance();
250
+ const updated = await pruneQueue.getTask(task.id);
251
+ expect(updated).toBeUndefined();
252
+ });
253
+ it('should handle complete with token mismatch', async () => {
254
+ const task = await queue.enqueue('token-mismatch', {});
255
+ const dequeued = await queue.dequeue();
256
+ // Create a fake task with wrong token
257
+ const fakeTask = { ...dequeued, token: '00000000-0000-0000-0000-000000000000' };
258
+ // Should not throw and should not complete
259
+ await queue.complete(fakeTask);
260
+ const updated = await queue.getTask(task.id);
261
+ expect(updated?.status).toBe(TaskStatus.Running);
262
+ });
263
+ it('should handle rescheduleManyByTags for running tasks', async () => {
264
+ const task = await queue.enqueue('tag-resched', {}, { tags: ['t1'] });
265
+ await queue.dequeue(); // Now Running
266
+ const future = currentTimestamp() + 10000;
267
+ await queue.rescheduleManyByTags('t1', future);
268
+ const updated = await queue.getTask(task.id);
269
+ expect(updated?.status).toBe(TaskStatus.Pending);
270
+ expect(updated?.tries).toBe(0); // Refunded
271
+ });
272
+ it('should handle dequeueMany shortfall refund', async () => {
273
+ // We need to trigger tasks.length < rateLimitAcquired
274
+ // This happens if rate limiter allows N, but only M < N are actually available/dequeuable
275
+ await queue.enqueue('shortfall', {});
276
+ // rateLimit is 5 by default in our setup.
277
+ // If we try to dequeue 5, but only 1 exists, it should refund 4.
278
+ const tasks = await queue.dequeueMany(5);
279
+ expect(tasks).toHaveLength(1);
280
+ });
281
+ it('should handle CircuitBreaker Half-Open state in dequeue', async () => {
282
+ const namespace = `cb-half-${Date.now()}`;
283
+ const cbQueue = injector.resolve(TaskQueueProvider).get(namespace, { circuitBreakerThreshold: 1 });
284
+ const cb = injector.resolve(CircuitBreaker, namespace);
285
+ // Mock CB to be Half-Open
286
+ vi.spyOn(cb, 'check').mockResolvedValue({ allowed: true, state: CircuitBreakerState.HalfOpen, isProbe: true });
287
+ await cbQueue.enqueue('cb', {});
288
+ const tasks = await cbQueue.dequeue();
289
+ expect(tasks).toBeDefined();
290
+ });
291
+ it('should handle CircuitBreaker Half-Open state blocking when already probing', async () => {
292
+ const namespace = `cb-block-${Date.now()}`;
293
+ const cbQueue = injector.resolve(TaskQueueProvider).get(namespace, { circuitBreakerThreshold: 1 });
294
+ const cb = injector.resolve(CircuitBreaker, namespace);
295
+ // Mock CB to be Half-Open but NOT a probe (already probing)
296
+ vi.spyOn(cb, 'check').mockResolvedValue({ allowed: true, state: CircuitBreakerState.HalfOpen, isProbe: false });
297
+ await cbQueue.enqueue('cb', {});
298
+ await cbQueue.enqueue('cb2', {});
299
+ // 1. Mock one task as already running in this namespace
300
+ // Actually, we can just enqueue and dequeue one normally first
301
+ const d1 = await cbQueue.dequeue({ forceDequeue: true });
302
+ expect(d1).toBeDefined();
303
+ // 2. Now the "normal" dequeue should see a running task and block because it's half-open and not a probe
304
+ const d2 = await cbQueue.dequeue();
305
+ expect(d2).toBeUndefined();
306
+ });
307
+ it('should handle complete when task has active children', async () => {
308
+ const parent = await queue.enqueue('parent', {});
309
+ await queue.enqueue('child', {}, { parentId: parent.id });
310
+ const dParent = await queue.dequeue();
311
+ await queue.complete(dParent);
312
+ const updated = await queue.getTask(parent.id);
313
+ expect(updated?.status).toBe(TaskStatus.WaitingChildren);
314
+ });
315
+ it('should handle maintenance recovering zombie tasks via touchMany logic in prune', async () => {
316
+ const pruneQueue = injector.resolve(TaskQueueProvider).get(`prune-fail-${Date.now()}`, { maxExecutionTime: 50, retention: 0 });
317
+ const task = await pruneQueue.enqueue('p1', {});
318
+ await pruneQueue.dequeue();
319
+ await timeout(100);
320
+ await pruneQueue.maintenance();
321
+ const updated = await pruneQueue.getTask(task.id);
322
+ expect(updated?.status).toBe(TaskStatus.Dead);
323
+ });
324
+ it('should handle enqueueMany with parentId and waitForCompletion false', async () => {
325
+ const parent = await queue.enqueue('p', {});
326
+ await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, waitForCompletion: false }]);
327
+ });
328
+ it('should handle failMany for running tasks', async () => {
329
+ const t1 = await queue.enqueue('t1', {});
330
+ const d1 = await queue.dequeue();
331
+ await queue.failMany([d1], [new Error('fail')]);
332
+ const updated = await queue.getTask(t1.id);
333
+ expect(updated?.status).toBe(TaskStatus.Pending);
334
+ expect(updated?.tries).toBe(1);
335
+ });
336
+ it('should handle failMany reaching max tries (Dead state)', async () => {
337
+ const isolatedQueue = injector.resolve(TaskQueueProvider).get(`exhaust-${Date.now()}`, { maxTries: 1 });
338
+ const t1 = await isolatedQueue.enqueue('t1', {});
339
+ const d1 = await isolatedQueue.dequeue();
340
+ await isolatedQueue.failMany([d1], [new Error('fatal')]);
341
+ const updated = await isolatedQueue.getTask(t1.id);
342
+ expect(updated?.status).toBe(TaskStatus.Dead);
343
+ });
344
+ it('should handle rescheduleMany with mixed statuses', async () => {
345
+ const t1 = await queue.enqueue('t1', {}); // Pending
346
+ const t2 = await queue.enqueue('t2', {});
347
+ await queue.dequeue(); // t2 is now Running
348
+ const future = currentTimestamp() + 10000;
349
+ await queue.rescheduleMany([t1.id, t2.id], future);
350
+ const u1 = await queue.getTask(t1.id);
351
+ const u2 = await queue.getTask(t2.id);
352
+ expect(u1?.status).toBe(TaskStatus.Pending);
353
+ expect(u1?.tries).toBe(0);
354
+ expect(u2?.status).toBe(TaskStatus.Pending);
355
+ expect(u2?.tries).toBe(0); // Refunded because it was Running
356
+ });
357
+ it('should handle restart with resetState', async () => {
358
+ const task = await queue.enqueue('t', { initial: 1 });
359
+ // Keep it Pending
360
+ await queue.restart(task.id, { resetState: true });
361
+ const updated = await queue.getTask(task.id);
362
+ expect(updated?.status).toBe(TaskStatus.Pending);
363
+ expect(updated?.state).toBeNull();
364
+ });
365
+ it('should handle clear with active tasks', async () => {
366
+ const t = await queue.enqueue('t', {});
367
+ await queue.clear();
368
+ expect(await queue.has(t.id)).toBe(false);
369
+ });
370
+ it('should handle countByTags with tags', async () => {
371
+ await queue.enqueue('t', {}, { tags: ['tag1', 'tag2'] });
372
+ expect(await queue.countByTags(['tag1'])).toBe(1);
373
+ expect(await queue.countByTags(['tag3'])).toBe(0);
374
+ });
375
+ it('should handle rescheduleManyByTags with mixed statuses', async () => {
376
+ const t1 = await queue.enqueue('t1', {}, { tags: ['tag-m'] });
377
+ const t2 = await queue.enqueue('t2', {}, { tags: ['tag-m'] });
378
+ await queue.dequeue(); // one is running
379
+ await queue.rescheduleManyByTags('tag-m', currentTimestamp() + 5000);
380
+ const u1 = await queue.getTask(t1.id);
381
+ const u2 = await queue.getTask(t2.id);
382
+ expect(u1?.tries).toBe(0);
383
+ expect(u2?.tries).toBe(0);
384
+ });
385
+ it('should handle touchMany with mismatched status/token', async () => {
386
+ const t1 = await queue.enqueue('t1', {});
387
+ const d1 = await queue.dequeue();
388
+ // Wrong token
389
+ const result = await queue.touchMany([{ ...d1, token: '00000000-0000-0000-0000-000000000000' }]);
390
+ expect(result).toHaveLength(0);
391
+ });
392
+ it('should handle large batch maintenance (> 1000 tasks)', async () => {
393
+ const namespace = `aging-batch-${Date.now()}`;
394
+ const agingQueue = injector.resolve(TaskQueueProvider).get(namespace, { priorityAgingInterval: 60000, priorityAgingStep: 1 });
395
+ const items = Array.from({ length: 1001 }, (_, i) => ({ type: 'batch', data: { i } }));
396
+ await agingQueue.enqueueMany(items);
397
+ // Manually push timestamps into the past to qualify for aging without waiting
398
+ const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
399
+ const twoMinutesAgo = currentTimestamp() - 120000;
400
+ await repository.updateManyByQuery({ namespace }, { priorityAgeTimestamp: twoMinutesAgo });
401
+ await agingQueue.maintenance();
402
+ });
403
+ it('should handle maintenance with empty archive and active tables', async () => {
404
+ const emptyQueue = injector.resolve(TaskQueueProvider).get(`empty-${Date.now()}`, { retention: 0, archiveRetention: 0 });
405
+ await emptyQueue.maintenance();
406
+ });
407
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3
+ import { CancellationToken } from '../../cancellation/index.js';
4
+ import { runInInjectionContext } from '../../injector/index.js';
5
+ import { Logger } from '../../logger/index.js';
6
+ import { RANDOM_UUID_V4 } from '../../orm/index.js';
7
+ import { injectRepository } from '../../orm/server/index.js';
8
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
9
+ import { task as taskTable } from '../../task-queue/postgres/schemas.js';
10
+ import { PostgresTask } from '../../task-queue/postgres/task.model.js';
11
+ import { TaskContext } from '../../task-queue/task-context.js';
12
+ import { setupIntegrationTest } from '../../testing/index.js';
13
+ import { timeout } from '../../utils/timing.js';
14
+ describe('Task Queue Coverage Enhancement', () => {
15
+ let injector;
16
+ let queueProvider;
17
+ beforeAll(async () => {
18
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
19
+ queueProvider = injector.resolve(TaskQueueProvider);
20
+ });
21
+ afterAll(async () => {
22
+ await injector?.dispose();
23
+ });
24
+ it('should exercise TaskContext getters and methods', async () => {
25
+ const queue = queueProvider.get(`context-test-${Date.now()}-${Math.random()}`);
26
+ const task = await queue.enqueue('test', { foo: 'bar' }, { tags: ['tag1'], priority: 500 });
27
+ const signal = new CancellationToken();
28
+ const logger = injector.resolve(Logger);
29
+ const context = new TaskContext(queue, task, signal, logger);
30
+ expect(context.id).toBe(task.id);
31
+ expect(context.type).toBe('test');
32
+ expect(context.parentId).toBeNull();
33
+ expect(context.tags).toContain('tag1');
34
+ expect(context.unresolvedScheduleDependencies).toBe(0);
35
+ expect(context.unresolvedCompleteDependencies).toBe(0);
36
+ expect(context.data).toEqual({ foo: 'bar' });
37
+ expect(context.state).toBeNull();
38
+ expect(context.attempt).toBe(0);
39
+ expect(context.triesLeft).toBe(queue.maxTries);
40
+ expect(context.isFinalAttempt).toBe(false);
41
+ expect(context.signal).toBe(signal);
42
+ expect(context.logger).toBeDefined();
43
+ // spawn for other queue
44
+ const otherQueue = queueProvider.get(`other-queue-${Date.now()}-${Math.random()}`);
45
+ const spawned = await context.spawn(otherQueue, 'spawned', { x: 1 });
46
+ expect(spawned.type).toBe('spawned');
47
+ const spawnedMany = await context.spawnMany(otherQueue, [{ type: 'm1', data: {} }, { type: 'm2', data: {} }]);
48
+ expect(spawnedMany).toHaveLength(2);
49
+ // checkpoint
50
+ await context.checkpoint({ progress: 0.5 });
51
+ // reschedule with delay
52
+ await context.reschedule({ delay: 10000 });
53
+ const uTask = await queue.getTask(task.id);
54
+ expect(uTask?.status).toBe(TaskStatus.Pending);
55
+ });
56
+ it('should handle checkpoint lease loss', async () => {
57
+ const queue = queueProvider.get(`checkpoint-loss-${Date.now()}-${Math.random()}`);
58
+ const task = await queue.enqueue('t', {});
59
+ const dTask = await queue.dequeue();
60
+ // Manually break the token in DB
61
+ const repository = runInInjectionContext(injector, () => injectRepository(PostgresTask));
62
+ await repository.useTransaction(undefined, async (tx) => {
63
+ await tx.pgTransaction.update(taskTable).set({ token: RANDOM_UUID_V4 }).where(eq(taskTable.id, task.id));
64
+ });
65
+ const signal = new CancellationToken();
66
+ const logger = injector.resolve(Logger);
67
+ const context = new TaskContext(queue, dTask, signal, logger);
68
+ await context.checkpoint({ progress: 0.9 });
69
+ expect(signal.isSet).toBe(true);
70
+ });
71
+ it('should exercise various TaskQueue methods', async () => {
72
+ const queue = queueProvider.get(`methods-test-${Date.now()}-${Math.random()}`);
73
+ // has
74
+ expect(await queue.has('00000000-0000-0000-0000-000000000000')).toBe(false);
75
+ const t = await queue.enqueue('t', {});
76
+ expect(await queue.has(t.id)).toBe(true);
77
+ // countByTags
78
+ expect(await queue.countByTags([])).toBe(0);
79
+ expect(await queue.countByTags(['tag1'])).toBe(0);
80
+ await queue.enqueue('t2', {}, { tags: ['tag1'] });
81
+ expect(await queue.countByTags(['tag1'])).toBe(1);
82
+ // getManyByTags
83
+ expect(await queue.getManyByTags([])).toEqual([]);
84
+ const tagged = await queue.getManyByTags(['tag1']);
85
+ expect(tagged).toHaveLength(1);
86
+ // clear
87
+ await queue.clear();
88
+ expect(await queue.countByTags(['tag1'])).toBe(0);
89
+ });
90
+ it('should load task from archive in getTask', async () => {
91
+ const archiveQueue = queueProvider.get(`archive-test-${Date.now()}-${Math.random()}`, { retention: 0, archiveRetention: 3600000 });
92
+ const task = await archiveQueue.enqueue('t', {});
93
+ const dTask = await archiveQueue.dequeue();
94
+ await archiveQueue.complete(dTask);
95
+ await timeout(10);
96
+ await archiveQueue.maintenance();
97
+ const archivedTask = await archiveQueue.getTask(task.id);
98
+ expect(archivedTask).toBeDefined();
99
+ expect(archivedTask?.id).toBe(task.id);
100
+ expect(archivedTask?.status).toBe(TaskStatus.Completed);
101
+ });
102
+ it('should exercise touchMany and failMany', async () => {
103
+ const isolatedQueue = queueProvider.get(`touch-fail-queue-${Date.now()}-${Math.random()}`);
104
+ const t1 = await isolatedQueue.enqueue('t1', {});
105
+ const t2 = await isolatedQueue.enqueue('t2', {});
106
+ const dTasks = await isolatedQueue.dequeueMany(2);
107
+ expect(dTasks).toHaveLength(2);
108
+ const touched = await isolatedQueue.touchMany(dTasks, { progresses: [0.5, 0.8] });
109
+ expect(touched).toHaveLength(2);
110
+ expect(touched).toContain(t1.id);
111
+ expect(touched).toContain(t2.id);
112
+ await isolatedQueue.failMany(dTasks, [new Error('e1'), new Error('e2')]);
113
+ const u1 = await isolatedQueue.getTask(t1.id);
114
+ const u2 = await isolatedQueue.getTask(t2.id);
115
+ expect(u1?.status).toBe(TaskStatus.Pending);
116
+ expect(u2?.status).toBe(TaskStatus.Pending);
117
+ expect(u1?.tries).toBe(1);
118
+ });
119
+ it('should handle idempotency collisions in enqueueMany', async () => {
120
+ const queue = queueProvider.get(`idempotency-test-${Date.now()}-${Math.random()}`);
121
+ const key = `idempotent-${Date.now()}-${Math.random()}`;
122
+ await queue.enqueue('t1', { val: 1 }, { idempotencyKey: key });
123
+ // Enqueue again with same key, returnTasks: true
124
+ const tasks = await queue.enqueueMany([
125
+ { type: 't1', data: { val: 2 }, idempotencyKey: key },
126
+ { type: 't2', data: { val: 3 } }
127
+ ], { returnTasks: true });
128
+ expect(tasks).toHaveLength(2);
129
+ const rt1 = tasks.find((t) => t.idempotencyKey == key);
130
+ const rt2 = tasks.find((t) => t.idempotencyKey == null);
131
+ expect(rt1?.data).toEqual({ val: 1 }); // Original data preserved
132
+ expect(rt2?.data).toEqual({ val: 3 }); // New task created
133
+ });
134
+ it('should purge archive in maintenance', async () => {
135
+ const purgeQueue = queueProvider.get(`purge-test-${Date.now()}-${Math.random()}`, { retention: 0, archiveRetention: 0 });
136
+ const task = await purgeQueue.enqueue('t', {});
137
+ const dTask = await purgeQueue.dequeue();
138
+ await purgeQueue.complete(dTask);
139
+ await timeout(10);
140
+ await purgeQueue.maintenance(); // Should archive AND purge
141
+ const purgedTask = await purgeQueue.getTask(task.id);
142
+ expect(purgedTask).toBeUndefined();
143
+ });
144
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
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
+ describe('DAG Dependencies', () => {
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 = `dag-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 schedule a task only after dependency reaches required status (scheduleAfter)', async () => {
24
+ const prereq = await queue.enqueue('prereq', { val: 1 });
25
+ const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
26
+ scheduleAfter: [prereq.id],
27
+ });
28
+ expect(dependent.status).toBe(TaskStatus.Waiting);
29
+ const dequeued = await queue.dequeue({ types: ['prereq'] });
30
+ expect(dequeued?.id).toBe(prereq.id);
31
+ await queue.complete(dequeued);
32
+ // Dependent should transition to Pending.
33
+ // Let's dequeue and complete it to make it reaches a finalized state for waitForTasks
34
+ const dDependent = await queue.dequeue({ types: ['dependent'] });
35
+ expect(dDependent?.id).toBe(dependent.id);
36
+ await queue.complete(dDependent);
37
+ await queue.waitForTasks([dependent.id]);
38
+ const updatedDependent = await queue.getTask(dependent.id);
39
+ expect(updatedDependent?.status).toBe(TaskStatus.Completed);
40
+ });
41
+ });