@tstdl/base 0.93.86 → 0.93.89

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 (318) hide show
  1. package/ai/genkit/helpers.d.ts +3 -1
  2. package/ai/genkit/helpers.js +3 -3
  3. package/api/server/gateway.d.ts +3 -0
  4. package/api/server/gateway.js +15 -4
  5. package/api/server/middlewares/catch-error.middleware.js +2 -4
  6. package/api/server/middlewares/cors.middleware.js +2 -3
  7. package/api/server/middlewares/csrf.middleware.d.ts +41 -0
  8. package/api/server/middlewares/csrf.middleware.js +108 -0
  9. package/api/server/middlewares/index.d.ts +1 -0
  10. package/api/server/middlewares/index.js +1 -0
  11. package/api/server/module.d.ts +8 -2
  12. package/api/server/module.js +14 -8
  13. package/api/server/tests/csrf.middleware.test.js +91 -0
  14. package/audit/drizzle/{0000_bored_stick.sql → 0000_lumpy_thunderball.sql} +3 -3
  15. package/audit/drizzle/meta/0000_snapshot.json +4 -4
  16. package/audit/drizzle/meta/_journal.json +2 -9
  17. package/audit/module.d.ts +4 -1
  18. package/audit/module.js +3 -2
  19. package/audit/schemas.d.ts +1 -1
  20. package/audit/types.d.ts +1 -1
  21. package/audit/types.js +1 -1
  22. package/authentication/client/authentication.service.d.ts +14 -1
  23. package/authentication/client/authentication.service.js +82 -23
  24. package/authentication/client/http-client.middleware.d.ts +6 -0
  25. package/authentication/client/http-client.middleware.js +36 -0
  26. package/authentication/client/module.js +8 -2
  27. package/authentication/models/service-account.model.d.ts +2 -2
  28. package/authentication/models/service-account.model.js +10 -5
  29. package/authentication/models/subject.model.d.ts +19 -5
  30. package/authentication/models/subject.model.js +25 -29
  31. package/authentication/models/system-account.model.d.ts +3 -2
  32. package/authentication/models/system-account.model.js +11 -5
  33. package/authentication/models/user.model.d.ts +2 -11
  34. package/authentication/models/user.model.js +5 -16
  35. package/authentication/server/authentication-api-request-token.provider.d.ts +0 -2
  36. package/authentication/server/authentication-api-request-token.provider.js +3 -11
  37. package/authentication/server/authentication.api-controller.d.ts +1 -2
  38. package/authentication/server/authentication.api-controller.js +8 -9
  39. package/authentication/server/authentication.audit.d.ts +3 -2
  40. package/authentication/server/authentication.service.d.ts +27 -1
  41. package/authentication/server/authentication.service.js +67 -18
  42. package/authentication/server/drizzle/{0000_normal_paper_doll.sql → 0000_soft_tag.sql} +25 -32
  43. package/authentication/server/drizzle/meta/0000_snapshot.json +180 -205
  44. package/authentication/server/drizzle/meta/_journal.json +2 -2
  45. package/authentication/server/helper.js +9 -2
  46. package/authentication/server/module.d.ts +4 -1
  47. package/authentication/server/module.js +9 -5
  48. package/authentication/server/schemas.d.ts +2 -1
  49. package/authentication/server/schemas.js +2 -2
  50. package/authentication/server/subject.service.d.ts +14 -8
  51. package/authentication/server/subject.service.js +86 -84
  52. package/authentication/tests/authentication-ancillary.service.test.d.ts +1 -0
  53. package/authentication/tests/authentication-ancillary.service.test.js +13 -0
  54. package/authentication/tests/authentication-secret-requirements.validator.test.d.ts +1 -0
  55. package/authentication/tests/authentication-secret-requirements.validator.test.js +29 -0
  56. package/authentication/tests/authentication.api-controller.test.d.ts +1 -0
  57. package/authentication/tests/authentication.api-controller.test.js +88 -0
  58. package/authentication/tests/authentication.api-request-token.provider.test.d.ts +1 -0
  59. package/authentication/tests/authentication.api-request-token.provider.test.js +48 -0
  60. package/authentication/tests/authentication.client-middleware.test.d.ts +1 -0
  61. package/authentication/tests/authentication.client-middleware.test.js +23 -0
  62. package/authentication/tests/authentication.client-service.test.d.ts +1 -0
  63. package/authentication/tests/authentication.client-service.test.js +70 -0
  64. package/authentication/tests/authentication.service.test.d.ts +1 -0
  65. package/authentication/tests/authentication.service.test.js +186 -0
  66. package/authentication/tests/authentication.test-ancillary-service.d.ts +9 -0
  67. package/authentication/tests/authentication.test-ancillary-service.js +27 -0
  68. package/authentication/tests/helper.test.d.ts +1 -0
  69. package/authentication/tests/helper.test.js +107 -0
  70. package/authentication/tests/secret-requirements.error.test.d.ts +1 -0
  71. package/authentication/tests/secret-requirements.error.test.js +14 -0
  72. package/authentication/tests/subject.service.test.d.ts +1 -0
  73. package/authentication/tests/subject.service.test.js +140 -0
  74. package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +1 -1
  75. package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
  76. package/circuit-breaker/postgres/module.d.ts +7 -1
  77. package/circuit-breaker/postgres/module.js +8 -6
  78. package/circuit-breaker/tests/circuit-breaker.test.js +2 -22
  79. package/document-management/api/document-management.api.js +2 -6
  80. package/document-management/server/services/document-validation.service.js +6 -5
  81. package/document-management/server/services/document-workflow.service.js +5 -5
  82. package/document-management/service-models/document-folders.view-model.d.ts +5 -2
  83. package/document-management/service-models/document-folders.view-model.js +42 -9
  84. package/document-management/service-models/enriched/enriched-document-management-data.view.js +1 -1
  85. package/examples/document-management/main.js +4 -4
  86. package/http/client/adapters/undici.adapter.d.ts +7 -5
  87. package/http/client/adapters/undici.adapter.js +13 -10
  88. package/http/client/module.d.ts +3 -1
  89. package/http/client/module.js +8 -9
  90. package/http/server/http-server.d.ts +2 -0
  91. package/http/server/node/module.d.ts +6 -2
  92. package/http/server/node/module.js +6 -4
  93. package/http/server/node/node-http-server.d.ts +2 -0
  94. package/http/server/node/node-http-server.js +7 -0
  95. package/http/types.d.ts +1 -1
  96. package/key-value-store/postgres/module.d.ts +7 -1
  97. package/key-value-store/postgres/module.js +7 -3
  98. package/lock/postgres/lock.js +0 -1
  99. package/lock/postgres/module.d.ts +7 -1
  100. package/lock/postgres/module.js +9 -5
  101. package/logger/formatter.d.ts +2 -0
  102. package/logger/formatters/json.js +2 -2
  103. package/logger/formatters/pretty-print.js +8 -10
  104. package/logger/logger.d.ts +1 -1
  105. package/logger/logger.js +15 -12
  106. package/message-bus/local/module.d.ts +5 -2
  107. package/message-bus/local/module.js +5 -4
  108. package/module/module.d.ts +2 -1
  109. package/module/module.js +3 -0
  110. package/module/modules/web-server.module.d.ts +11 -6
  111. package/module/modules/web-server.module.js +15 -10
  112. package/orm/decorators.d.ts +24 -1
  113. package/orm/decorators.js +40 -4
  114. package/orm/index.d.ts +1 -1
  115. package/orm/index.js +1 -1
  116. package/orm/query/base.d.ts +17 -17
  117. package/orm/query/base.js +1 -1
  118. package/orm/repository.types.d.ts +46 -2
  119. package/orm/schemas/tsvector.js +1 -1
  120. package/orm/server/drizzle/schema-converter.d.ts +3 -1
  121. package/orm/server/drizzle/schema-converter.js +120 -14
  122. package/orm/server/index.d.ts +1 -0
  123. package/orm/server/index.js +1 -0
  124. package/orm/server/module.d.ts +4 -2
  125. package/orm/server/module.js +6 -5
  126. package/orm/server/query-converter.d.ts +6 -3
  127. package/orm/server/query-converter.js +33 -21
  128. package/orm/server/repository-config.d.ts +8 -0
  129. package/orm/server/repository-config.js +8 -0
  130. package/orm/server/repository.d.ts +117 -43
  131. package/orm/server/repository.js +758 -254
  132. package/orm/server/transaction.d.ts +4 -2
  133. package/orm/server/transaction.js +14 -5
  134. package/orm/server/transactional.d.ts +6 -2
  135. package/orm/server/transactional.js +39 -9
  136. package/orm/server/types.d.ts +2 -0
  137. package/orm/sqls/case-when.d.ts +25 -0
  138. package/orm/sqls/case-when.js +54 -0
  139. package/orm/sqls/index.d.ts +2 -0
  140. package/orm/sqls/index.js +2 -0
  141. package/orm/{sqls.d.ts → sqls/sqls.d.ts} +67 -19
  142. package/orm/{sqls.js → sqls/sqls.js} +116 -22
  143. package/orm/tests/data-types.test.d.ts +1 -0
  144. package/orm/tests/data-types.test.js +39 -0
  145. package/orm/tests/decorators.test.d.ts +1 -0
  146. package/orm/tests/decorators.test.js +77 -0
  147. package/orm/tests/encryption.test.d.ts +1 -0
  148. package/orm/tests/encryption.test.js +34 -0
  149. package/orm/tests/query-complex.test.d.ts +1 -0
  150. package/orm/tests/query-complex.test.js +203 -0
  151. package/orm/tests/query-converter-complex.test.d.ts +1 -0
  152. package/orm/tests/query-converter-complex.test.js +126 -0
  153. package/orm/tests/query-converter.test.d.ts +1 -0
  154. package/orm/tests/query-converter.test.js +123 -0
  155. package/orm/tests/repository-advanced.test.d.ts +1 -0
  156. package/orm/tests/repository-advanced.test.js +232 -0
  157. package/orm/tests/repository-attributes.test.d.ts +1 -0
  158. package/orm/tests/repository-attributes.test.js +99 -0
  159. package/orm/tests/repository-comprehensive.test.d.ts +1 -0
  160. package/orm/tests/repository-comprehensive.test.js +187 -0
  161. package/orm/tests/repository-coverage.test.d.ts +1 -0
  162. package/orm/tests/repository-coverage.test.js +303 -0
  163. package/orm/tests/repository-cti-complex.test.d.ts +1 -0
  164. package/orm/tests/repository-cti-complex.test.js +170 -0
  165. package/orm/tests/repository-cti-embedded.test.d.ts +1 -0
  166. package/orm/tests/repository-cti-embedded.test.js +188 -0
  167. package/orm/tests/repository-cti-extensive.test.d.ts +1 -0
  168. package/orm/tests/repository-cti-extensive.test.js +308 -0
  169. package/orm/tests/repository-cti-mapping.test.d.ts +1 -0
  170. package/orm/tests/repository-cti-mapping.test.js +121 -0
  171. package/orm/tests/repository-cti-search.test.d.ts +1 -0
  172. package/orm/tests/repository-cti-search.test.js +152 -0
  173. package/orm/tests/repository-cti-soft-delete.test.d.ts +1 -0
  174. package/orm/tests/repository-cti-soft-delete.test.js +115 -0
  175. package/orm/tests/repository-cti-transactions.test.d.ts +1 -0
  176. package/orm/tests/repository-cti-transactions.test.js +126 -0
  177. package/orm/tests/repository-cti-upsert-many.test.d.ts +1 -0
  178. package/orm/tests/repository-cti-upsert-many.test.js +127 -0
  179. package/orm/tests/repository-cti.test.d.ts +1 -0
  180. package/orm/tests/repository-cti.test.js +456 -0
  181. package/orm/tests/repository-edge-cases.test.d.ts +1 -0
  182. package/orm/tests/repository-edge-cases.test.js +216 -0
  183. package/orm/tests/repository-expiration.test.d.ts +1 -0
  184. package/orm/tests/repository-expiration.test.js +153 -0
  185. package/orm/tests/repository-extra-coverage.test.d.ts +1 -0
  186. package/orm/tests/repository-extra-coverage.test.js +546 -0
  187. package/orm/tests/repository-mapping.test.d.ts +1 -0
  188. package/orm/tests/repository-mapping.test.js +71 -0
  189. package/orm/tests/repository-regression.test.d.ts +1 -0
  190. package/orm/tests/repository-regression.test.js +330 -0
  191. package/orm/tests/repository-search-coverage.test.d.ts +1 -0
  192. package/orm/tests/repository-search-coverage.test.js +129 -0
  193. package/orm/tests/repository-search.test.d.ts +1 -0
  194. package/orm/tests/repository-search.test.js +116 -0
  195. package/orm/tests/repository-soft-delete.test.d.ts +1 -0
  196. package/orm/tests/repository-soft-delete.test.js +143 -0
  197. package/orm/tests/repository-transactions-nested.test.d.ts +1 -0
  198. package/orm/tests/repository-transactions-nested.test.js +202 -0
  199. package/orm/tests/repository-types.test.d.ts +1 -0
  200. package/orm/tests/repository-types.test.js +218 -0
  201. package/orm/tests/schema-converter.test.d.ts +1 -0
  202. package/orm/tests/schema-converter.test.js +81 -0
  203. package/orm/tests/schema-generation.test.d.ts +1 -0
  204. package/orm/tests/schema-generation.test.js +127 -0
  205. package/orm/tests/sql-helpers.test.d.ts +1 -0
  206. package/orm/tests/sql-helpers.test.js +67 -0
  207. package/orm/tests/transaction-safety.test.d.ts +1 -0
  208. package/orm/tests/transaction-safety.test.js +81 -0
  209. package/orm/tests/transactional.test.d.ts +1 -0
  210. package/orm/tests/transactional.test.js +224 -0
  211. package/orm/tests/utils.test.d.ts +1 -0
  212. package/orm/tests/utils.test.js +70 -0
  213. package/orm/utils.d.ts +7 -0
  214. package/orm/utils.js +26 -6
  215. package/package.json +12 -7
  216. package/pool/pool.js +1 -1
  217. package/rate-limit/index.d.ts +2 -0
  218. package/rate-limit/index.js +2 -0
  219. package/rate-limit/postgres/drizzle/0000_watery_rage.sql +7 -0
  220. package/{queue → rate-limit}/postgres/drizzle/meta/0000_snapshot.json +14 -39
  221. package/rate-limit/postgres/drizzle/meta/_journal.json +13 -0
  222. package/{queue → rate-limit}/postgres/drizzle.config.js +1 -1
  223. package/rate-limit/postgres/index.d.ts +4 -0
  224. package/rate-limit/postgres/index.js +4 -0
  225. package/rate-limit/postgres/module.d.ts +12 -0
  226. package/rate-limit/postgres/module.js +28 -0
  227. package/rate-limit/postgres/postgres-rate-limiter.d.ts +9 -0
  228. package/rate-limit/postgres/postgres-rate-limiter.js +56 -0
  229. package/rate-limit/postgres/rate-limit.model.d.ts +8 -0
  230. package/rate-limit/postgres/rate-limit.model.js +35 -0
  231. package/rate-limit/postgres/rate-limiter.provider.d.ts +6 -0
  232. package/rate-limit/postgres/rate-limiter.provider.js +21 -0
  233. package/rate-limit/postgres/schemas.d.ts +3 -0
  234. package/rate-limit/postgres/schemas.js +4 -0
  235. package/rate-limit/provider.d.ts +9 -0
  236. package/rate-limit/provider.js +2 -0
  237. package/rate-limit/rate-limiter.d.ts +35 -0
  238. package/rate-limit/rate-limiter.js +3 -0
  239. package/rate-limit/tests/postgres-rate-limiter.test.d.ts +1 -0
  240. package/rate-limit/tests/postgres-rate-limiter.test.js +92 -0
  241. package/signals/implementation/configure.d.ts +3 -0
  242. package/signals/implementation/configure.js +3 -0
  243. package/sse/data-stream-source.d.ts +1 -1
  244. package/sse/data-stream-source.js +6 -6
  245. package/task-queue/enqueue-batch.d.ts +17 -0
  246. package/task-queue/enqueue-batch.js +24 -0
  247. package/{queue → task-queue}/index.d.ts +1 -1
  248. package/{queue → task-queue}/index.js +1 -1
  249. package/task-queue/postgres/drizzle/0000_thin_black_panther.sql +74 -0
  250. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +592 -0
  251. package/task-queue/postgres/drizzle/meta/_journal.json +13 -0
  252. package/task-queue/postgres/drizzle.config.d.ts +2 -0
  253. package/task-queue/postgres/drizzle.config.js +11 -0
  254. package/task-queue/postgres/index.d.ts +4 -0
  255. package/task-queue/postgres/index.js +4 -0
  256. package/task-queue/postgres/module.d.ts +12 -0
  257. package/task-queue/postgres/module.js +28 -0
  258. package/task-queue/postgres/schemas.d.ts +16 -0
  259. package/task-queue/postgres/schemas.js +8 -0
  260. package/task-queue/postgres/task-queue.d.ts +83 -0
  261. package/task-queue/postgres/task-queue.js +1054 -0
  262. package/task-queue/postgres/task-queue.provider.d.ts +7 -0
  263. package/{queue/postgres/queue.provider.js → task-queue/postgres/task-queue.provider.js} +8 -8
  264. package/task-queue/postgres/task.model.d.ts +39 -0
  265. package/task-queue/postgres/task.model.js +178 -0
  266. package/{queue → task-queue}/provider.d.ts +3 -3
  267. package/task-queue/provider.js +2 -0
  268. package/{queue → task-queue}/task-context.d.ts +7 -7
  269. package/{queue → task-queue}/task-context.js +8 -8
  270. package/{queue/queue.d.ts → task-queue/task-queue.d.ts} +128 -59
  271. package/task-queue/task-queue.js +200 -0
  272. package/task-queue/tests/complex.test.d.ts +1 -0
  273. package/task-queue/tests/complex.test.js +299 -0
  274. package/task-queue/tests/dependencies.test.d.ts +1 -0
  275. package/task-queue/tests/dependencies.test.js +174 -0
  276. package/task-queue/tests/queue.test.d.ts +1 -0
  277. package/task-queue/tests/queue.test.js +334 -0
  278. package/task-queue/tests/worker.test.d.ts +1 -0
  279. package/task-queue/tests/worker.test.js +163 -0
  280. package/test1.js +1 -1
  281. package/test4.js +2 -2
  282. package/unit-test/index.d.ts +1 -0
  283. package/unit-test/index.js +1 -0
  284. package/unit-test/integration-setup.d.ts +55 -0
  285. package/unit-test/integration-setup.js +182 -0
  286. package/utils/patterns.d.ts +3 -0
  287. package/utils/patterns.js +6 -1
  288. package/audit/drizzle/0001_previous_network.sql +0 -2
  289. package/audit/drizzle/meta/0001_snapshot.json +0 -195
  290. package/queue/enqueue-batch.d.ts +0 -17
  291. package/queue/enqueue-batch.js +0 -18
  292. package/queue/postgres/drizzle/0000_zippy_moondragon.sql +0 -11
  293. package/queue/postgres/drizzle/0001_certain_wild_pack.sql +0 -2
  294. package/queue/postgres/drizzle/0002_dear_meggan.sql +0 -2
  295. package/queue/postgres/drizzle/0003_tricky_venom.sql +0 -30
  296. package/queue/postgres/drizzle/meta/0001_snapshot.json +0 -103
  297. package/queue/postgres/drizzle/meta/0002_snapshot.json +0 -90
  298. package/queue/postgres/drizzle/meta/0003_snapshot.json +0 -288
  299. package/queue/postgres/drizzle/meta/_journal.json +0 -34
  300. package/queue/postgres/index.d.ts +0 -4
  301. package/queue/postgres/index.js +0 -4
  302. package/queue/postgres/module.d.ts +0 -9
  303. package/queue/postgres/module.js +0 -29
  304. package/queue/postgres/queue.d.ts +0 -60
  305. package/queue/postgres/queue.js +0 -681
  306. package/queue/postgres/queue.provider.d.ts +0 -7
  307. package/queue/postgres/schemas.d.ts +0 -14
  308. package/queue/postgres/schemas.js +0 -6
  309. package/queue/postgres/task.model.d.ts +0 -24
  310. package/queue/postgres/task.model.js +0 -115
  311. package/queue/provider.js +0 -2
  312. package/queue/queue.js +0 -131
  313. package/queue/tests/queue.test.js +0 -623
  314. package/test3.d.ts +0 -1
  315. package/test3.js +0 -47
  316. /package/{queue/tests/queue.test.d.ts → api/server/tests/csrf.middleware.test.d.ts} +0 -0
  317. /package/circuit-breaker/postgres/drizzle/{0000_hard_shocker.sql → 0000_cooing_korath.sql} +0 -0
  318. /package/{queue → rate-limit}/postgres/drizzle.config.d.ts +0 -0
@@ -0,0 +1,174 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { CancellationToken } from '../../cancellation/index.js';
3
+ import { DependencyJoinMode, TaskQueueProvider, TaskState } from '../../task-queue/index.js';
4
+ import { setupIntegrationTest } from '../../unit-test/index.js';
5
+ import { timeout } from '../../utils/timing.js';
6
+ describe('Queue Dependencies & Tree Tests', () => {
7
+ let injector;
8
+ let queue;
9
+ beforeAll(async () => {
10
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
11
+ });
12
+ beforeEach(() => {
13
+ const queueProvider = injector.resolve(TaskQueueProvider);
14
+ const queueName = `dep-queue-${Date.now()}-${Math.random()}`;
15
+ queue = queueProvider.get(queueName, {
16
+ visibilityTimeout: 1000,
17
+ });
18
+ });
19
+ afterEach(async () => {
20
+ await queue.clear();
21
+ });
22
+ afterAll(async () => {
23
+ await injector?.dispose();
24
+ });
25
+ describe('Dependencies (Fan-In)', () => {
26
+ it('should schedule a task only after dependency completes (completeAfterTags)', async () => {
27
+ // 1. Create a dependent task (Waiting)
28
+ const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
29
+ completeAfterTags: ['tag-a'],
30
+ });
31
+ expect(dependent.status).toBe(TaskState.Waiting);
32
+ // 2. Create the prerequisite task
33
+ const prereq = await queue.enqueue('prereq', { val: 1 }, { tags: ['tag-a'] });
34
+ // 3. Complete prereq
35
+ const dequeued = await queue.dequeue({ types: ['prereq'] });
36
+ expect(dequeued?.id).toBe(prereq.id);
37
+ await queue.complete(dequeued);
38
+ await queue.processPendingFanIn();
39
+ const updatedDependent = await queue.getTask(dependent.id);
40
+ expect(updatedDependent?.status).toBe(TaskState.Completed);
41
+ });
42
+ it('should schedule a task to run after dependency completes (scheduleAfterTags)', async () => {
43
+ // 1. Dependent task
44
+ const dependent = await queue.enqueue('dependent', { foo: 'bar' }, {
45
+ scheduleAfterTags: ['tag-b'],
46
+ });
47
+ expect(dependent.status).toBe(TaskState.Waiting);
48
+ // 2. Prereq
49
+ const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-b'] });
50
+ // 3. Complete prereq
51
+ const dequeued = await queue.dequeue({ types: ['prereq'] });
52
+ await queue.complete(dequeued);
53
+ await queue.processPendingFanIn();
54
+ // 5. Dependent should be Pending (ready to run)
55
+ const updatedDependent = await queue.getTask(dependent.id);
56
+ expect(updatedDependent?.status).toBe(TaskState.Pending);
57
+ // 6. Should be dequeueable
58
+ const d2 = await queue.dequeue({ types: ['dependent'] });
59
+ expect(d2?.id).toBe(dependent.id);
60
+ });
61
+ it('should fail-fast if dependency fails', async () => {
62
+ const dependent = await queue.enqueue('dependent', {}, {
63
+ scheduleAfterTags: ['tag-fail'],
64
+ failFast: true
65
+ });
66
+ const prereq = await queue.enqueue('prereq', {}, { tags: ['tag-fail'] });
67
+ const dequeued = await queue.dequeue({ types: ['prereq'] });
68
+ // Fail fatally
69
+ await queue.fail(dequeued, new Error('boom'), true);
70
+ // Trigger resolution
71
+ await queue.processPendingFanIn();
72
+ let updatedDependent;
73
+ for (let i = 0; i < 20; i++) {
74
+ await timeout(100);
75
+ updatedDependent = await queue.getTask(dependent.id);
76
+ if (updatedDependent?.status === TaskState.Dead)
77
+ break;
78
+ await queue.processPendingFanIn(); // Retry processing if it didn't catch it yet
79
+ }
80
+ expect(updatedDependent?.status).toBe(TaskState.Dead);
81
+ expect(updatedDependent?.error?.code).toBe('DependencyFailed');
82
+ });
83
+ it('should respect DependencyJoinMode.Or', async () => {
84
+ // Wait for tag-1 OR tag-2
85
+ const dependent = await queue.enqueue('dependent', {}, {
86
+ scheduleAfterTags: ['tag-1', 'tag-2'],
87
+ dependencyJoinMode: DependencyJoinMode.Or
88
+ });
89
+ // Complete tag-1 only
90
+ await queue.enqueue('t1', {}, { tags: ['tag-1'] });
91
+ const d1 = await queue.dequeue({ types: ['t1'] });
92
+ await queue.complete(d1);
93
+ await queue.processPendingFanIn();
94
+ const updated = await queue.getTask(dependent.id);
95
+ expect(updated?.status).toBe(TaskState.Pending);
96
+ });
97
+ });
98
+ describe('Tree Operations & Cancellation', () => {
99
+ it('should retrieve task tree and cancel hierarchy', async () => {
100
+ // Root
101
+ const root = await queue.enqueue('root', {});
102
+ // Child
103
+ const child = await queue.enqueue('child', {}, { parentId: root.id });
104
+ // Grandchild
105
+ const grandchild = await queue.enqueue('grandchild', {}, { parentId: child.id });
106
+ // Get Tree
107
+ const tree = await queue.getTree(root.id);
108
+ expect(tree.length).toBe(3);
109
+ expect(tree.map(t => t.id)).toContain(grandchild.id);
110
+ // Cancel Root
111
+ await queue.cancel(root.id);
112
+ // Verify all cancelled
113
+ const tRoot = await queue.getTask(root.id);
114
+ const tChild = await queue.getTask(child.id);
115
+ const tGrand = await queue.getTask(grandchild.id);
116
+ expect(tRoot?.status).toBe(TaskState.Cancelled);
117
+ expect(tChild?.status).toBe(TaskState.Cancelled);
118
+ expect(tGrand?.status).toBe(TaskState.Cancelled);
119
+ });
120
+ it('should cancel many by tags', async () => {
121
+ await queue.enqueue('t1', {}, { tags: ['group-a'] });
122
+ await queue.enqueue('t2', {}, { tags: ['group-a'] });
123
+ await queue.enqueue('t3', {}, { tags: ['group-b'] });
124
+ await queue.cancelManyByTags(['group-a']);
125
+ // Check status
126
+ const tasks = await queue.getManyByTags('group-a');
127
+ expect(tasks.length).toBe(2);
128
+ expect(tasks.every(t => t.status === TaskState.Cancelled)).toBe(true);
129
+ const tasksB = await queue.getManyByTags('group-b');
130
+ expect(tasksB[0]?.status).toBe(TaskState.Pending);
131
+ });
132
+ });
133
+ describe('Restart & Consumers', () => {
134
+ it('should restart a dead/cancelled task', async () => {
135
+ const task = await queue.enqueue('restart-test', {});
136
+ const d = await queue.dequeue({ types: ['restart-test'] });
137
+ await queue.fail(d, new Error('fatal'), true); // Dead
138
+ await queue.restart(task.id);
139
+ const updated = await queue.getTask(task.id);
140
+ expect(updated?.status).toBe(TaskState.Pending);
141
+ expect(updated?.tries).toBe(0);
142
+ expect(updated?.error).toBeNull();
143
+ });
144
+ it('should consume tasks via async iterator (getConsumer)', async () => {
145
+ await queue.enqueue('c1', { val: 1 });
146
+ await queue.enqueue('c2', { val: 2 });
147
+ const token = new CancellationToken();
148
+ const consumer = queue.getConsumer(token);
149
+ const t1 = (await consumer.next()).value;
150
+ expect(t1.data.val).toBe(1);
151
+ const t2 = (await consumer.next()).value;
152
+ expect(t2.data.val).toBe(2);
153
+ token.set(); // Stop consumer
154
+ const t3 = await consumer.next();
155
+ expect(t3.done).toBe(true);
156
+ });
157
+ it('should consume batches via async iterator (getBatchConsumer)', async () => {
158
+ await queue.enqueueMany([
159
+ { type: 'b', data: { val: 1 } },
160
+ { type: 'b', data: { val: 2 } },
161
+ { type: 'b', data: { val: 3 } }
162
+ ]);
163
+ const token = new CancellationToken();
164
+ const batchConsumer = queue.getBatchConsumer(2, token);
165
+ const b1 = (await batchConsumer.next()).value;
166
+ expect(b1.length).toBe(2);
167
+ const b2 = (await batchConsumer.next()).value;
168
+ expect(b2.length).toBe(1);
169
+ token.set();
170
+ const b3 = await batchConsumer.next();
171
+ expect(b3.done).toBe(true);
172
+ });
173
+ });
174
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,334 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { TaskQueueProvider, TaskState } from '../../task-queue/index.js';
3
+ import { setupIntegrationTest } from '../../unit-test/index.js';
4
+ import { currentTimestamp } from '../../utils/date-time.js';
5
+ import { timeout } from '../../utils/timing.js';
6
+ describe('Queue Integration Tests', () => {
7
+ let injector;
8
+ let queue;
9
+ const queueName = `test-queue-${Date.now()}`;
10
+ // Helper to verify state in DB
11
+ async function assertTaskState(id, state, message) {
12
+ const task = await queue.getTask(id);
13
+ expect(task?.status, message).toBe(state);
14
+ }
15
+ beforeAll(async () => {
16
+ ({ 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.
20
+ const queueProvider = injector.resolve(TaskQueueProvider);
21
+ queue = queueProvider.get(queueName, {
22
+ visibilityTimeout: 5000,
23
+ retryDelayMinimum: 100, // Fast retries for testing
24
+ retryDelayGrowth: 1, // Linear/No growth for predictable tests
25
+ });
26
+ });
27
+ afterEach(async () => {
28
+ // Drain the queue to prevent state leakage between tests
29
+ await queue.clear();
30
+ });
31
+ afterAll(async () => {
32
+ try {
33
+ await queue.clear();
34
+ await injector.dispose();
35
+ }
36
+ catch (error) {
37
+ // Ignore known double-dispose issue from MessageBus interaction
38
+ if (error instanceof Error && error.message === 'MessageBus is disposed.') {
39
+ return;
40
+ }
41
+ throw error;
42
+ }
43
+ });
44
+ it('Basic FIFO Flow', async () => {
45
+ const t1 = await queue.enqueue('test', { value: 'first' });
46
+ const t2 = await queue.enqueue('test', { value: 'second' });
47
+ const d1 = await queue.dequeue();
48
+ expect(d1?.id).toBe(t1.id);
49
+ expect(d1?.data.value).toBe('first');
50
+ await queue.complete(d1, { success: true });
51
+ await assertTaskState(t1.id, TaskState.Completed, 'Task 1 completed');
52
+ const d2 = await queue.dequeue();
53
+ expect(d2?.id).toBe(t2.id);
54
+ await queue.complete(d2, { success: true });
55
+ });
56
+ it('Priorities', async () => {
57
+ // Priority 1000 (default)
58
+ const low = await queue.enqueue('test', { value: 'low' });
59
+ // Priority 1
60
+ const high = await queue.enqueue('test', { value: 'high' }, { priority: 1 });
61
+ const first = await queue.dequeue();
62
+ expect(first?.id).toBe(high.id);
63
+ await queue.complete(first);
64
+ const second = await queue.dequeue();
65
+ expect(second?.id).toBe(low.id);
66
+ await queue.complete(second);
67
+ });
68
+ it('Deduplication (Idempotency Keys)', async () => {
69
+ const key = `unique-${Date.now()}`;
70
+ // 1. Initial Insert
71
+ const t1 = await queue.enqueue('test', { value: 'original' }, { idempotencyKey: key });
72
+ // 2. Default Strategy (replace: false): Should return existing task, ignore new data
73
+ const t2 = await queue.enqueue('test', { value: 'ignored' }, { idempotencyKey: key });
74
+ expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
75
+ const check1 = await queue.getTask(t1.id);
76
+ expect(check1?.data.value).toBe('original');
77
+ // 3. Replace Strategy: Should replace existing task with new data AND new ID
78
+ 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);
80
+ // New task should have new data
81
+ const checkNew = await queue.getTask(t3[0].id);
82
+ expect(checkNew?.data.value).toBe('updated');
83
+ expect(checkNew?.tries).toBe(0);
84
+ });
85
+ it('Retries and Failures', async () => {
86
+ const task = await queue.enqueue('test', { value: 'fail-me' });
87
+ // Try 1
88
+ const attempt1 = await queue.dequeue();
89
+ expect(attempt1?.id).toBe(task.id);
90
+ await queue.fail(attempt1, { message: 'oops' });
91
+ // Force reschedule to now to bypass retryDelay
92
+ await queue.reschedule(task.id, Date.now());
93
+ // Try 2
94
+ const attempt2 = await queue.dequeue();
95
+ expect(attempt2?.id).toBe(task.id);
96
+ expect(attempt2?.tries).toBe(2);
97
+ // Fail fatally
98
+ await queue.fail(attempt2, { message: 'fatal error' }, true);
99
+ await assertTaskState(task.id, TaskState.Dead, 'Task is Dead after fatal error');
100
+ });
101
+ it('Hierarchy (Parent/Child)', async () => {
102
+ // A. Create Parent
103
+ const p = await queue.enqueue('test', { value: 'parent-manual' });
104
+ // B. Dequeue Parent
105
+ const pTask = await queue.dequeue();
106
+ expect(pTask?.id).toBe(p.id);
107
+ // C. Parent spawns child
108
+ const child = await queue.enqueue('test', { value: 'child-manual' }, { parentId: p.id });
109
+ // D. "Finish" Parent execution.
110
+ await queue.complete(pTask);
111
+ // await assertTaskState(p.id, TaskState.Waiting, 'Parent entered WAITING state'); // Depends on implementation details of auto-waiting
112
+ });
113
+ it('Batching', async () => {
114
+ const batch = queue.batch();
115
+ for (let i = 0; i < 5; i++) {
116
+ batch.add('test', { value: `batch-${i}` });
117
+ }
118
+ const tasks = await batch.enqueue(true);
119
+ expect(tasks.length).toBe(5);
120
+ const dequeuedBatch = await queue.dequeueMany(5);
121
+ expect(dequeuedBatch.length).toBe(5);
122
+ await queue.completeMany(dequeuedBatch);
123
+ const leftover = await queue.dequeue();
124
+ expect(leftover).toBeUndefined();
125
+ });
126
+ });
127
+ describe('PostgresQueue (Distributed Task Orchestration)', () => {
128
+ let injector;
129
+ let queue;
130
+ beforeAll(async () => {
131
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
132
+ });
133
+ beforeEach(() => {
134
+ const queueProvider = injector.resolve(TaskQueueProvider);
135
+ const queueName = `pg-test-queue-${Date.now()}-${Math.random()}`;
136
+ queue = queueProvider.get(queueName, {
137
+ visibilityTimeout: 200, // Short timeout for testing
138
+ retryDelayMinimum: 50,
139
+ retryDelayGrowth: 1,
140
+ circuitBreakerThreshold: 2,
141
+ circuitBreakerResetTimeout: 200,
142
+ });
143
+ });
144
+ afterEach(async () => {
145
+ await queue.clear();
146
+ });
147
+ afterAll(async () => {
148
+ await injector?.dispose();
149
+ });
150
+ describe('Basic Lifecycle', () => {
151
+ it('should enqueue and dequeue a task', async () => {
152
+ await queue.enqueue('test', { foo: 'bar' });
153
+ const task = await queue.dequeue();
154
+ expect(task).toBeDefined();
155
+ expect(task?.data).toEqual({ foo: 'bar' });
156
+ expect(task?.status).toBe(TaskState.Running);
157
+ expect(task?.tries).toBe(1);
158
+ });
159
+ it('should complete a task successfully', async () => {
160
+ const task = await queue.enqueue('test', { foo: 'bar' });
161
+ const dequeued = await queue.dequeue();
162
+ await queue.complete(dequeued, { result: true });
163
+ const updated = await queue.getTask(task.id);
164
+ expect(updated?.status).toBe(TaskState.Completed);
165
+ expect(updated?.result).toEqual({ result: true });
166
+ expect(updated?.completeTimestamp).toBeGreaterThan(0);
167
+ });
168
+ it('should fail a task and increment tries', async () => {
169
+ const task = await queue.enqueue('test', { foo: 'bar' });
170
+ const dequeued = await queue.dequeue();
171
+ await queue.fail(dequeued, new Error('temp failure'));
172
+ const updated = await queue.getTask(task.id);
173
+ expect(updated?.status).toBe(TaskState.Pending);
174
+ expect(updated?.tries).toBe(1);
175
+ expect(updated?.error).toBeDefined();
176
+ });
177
+ });
178
+ describe('Priority and Scheduling', () => {
179
+ it('should dequeue tasks in priority order (lower number first)', async () => {
180
+ await queue.enqueue('test', { foo: 'low' }, { priority: 2000 });
181
+ await queue.enqueue('test', { foo: 'high' }, { priority: 10 });
182
+ await queue.enqueue('test', { foo: 'mid' }, { priority: 1000 });
183
+ const t1 = await queue.dequeue();
184
+ const t2 = await queue.dequeue();
185
+ const t3 = await queue.dequeue();
186
+ expect(t1?.data?.foo).toBe('high');
187
+ expect(t2?.data?.foo).toBe('mid');
188
+ expect(t3?.data?.foo).toBe('low');
189
+ });
190
+ it('should not dequeue a task scheduled in the future', async () => {
191
+ const future = currentTimestamp() + 500;
192
+ await queue.enqueue('test', { foo: 'future' }, { scheduleTimestamp: future });
193
+ const task = await queue.dequeue();
194
+ expect(task).toBeUndefined();
195
+ await timeout(600);
196
+ const taskLater = await queue.dequeue();
197
+ expect(taskLater).toBeDefined();
198
+ });
199
+ });
200
+ describe('Concurrency Control', () => {
201
+ it('should respect global concurrency limits', async () => {
202
+ const queueProvider = injector.resolve(TaskQueueProvider);
203
+ const limitedQueue = queueProvider.get(`limit-test-${Date.now()}`, { globalConcurrency: 2 });
204
+ await limitedQueue.enqueueMany([
205
+ { type: 'test', data: { foo: '1' } },
206
+ { type: 'test', data: { foo: '2' } },
207
+ { type: 'test', data: { foo: '3' } },
208
+ ]);
209
+ const t1 = await limitedQueue.dequeue();
210
+ const t2 = await limitedQueue.dequeue();
211
+ const t3 = await limitedQueue.dequeue();
212
+ expect(t1).toBeDefined();
213
+ expect(t2).toBeDefined();
214
+ expect(t3).toBeUndefined(); // Limit reached
215
+ await limitedQueue.complete(t1);
216
+ const t3Retry = await limitedQueue.dequeue();
217
+ expect(t3Retry).toBeDefined(); // Slot opened
218
+ await limitedQueue.clear();
219
+ });
220
+ });
221
+ describe('Circuit Breaker', () => {
222
+ it('should trip the breaker after threshold failures', async () => {
223
+ // Config: circuitBreakerThreshold: 2 (set in beforeEach)
224
+ await queue.enqueue('test', { foo: '1' });
225
+ await queue.enqueue('test', { foo: '2' });
226
+ await queue.enqueue('test', { foo: '3' });
227
+ await queue.fail((await queue.dequeue()), 'err');
228
+ await queue.fail((await queue.dequeue()), 'err');
229
+ // Breaker should be Open
230
+ const t3Attempt = await queue.dequeue();
231
+ expect(t3Attempt).toBeUndefined();
232
+ });
233
+ it('should allow a single probe in Half-Open state', async () => {
234
+ await queue.enqueueMany([
235
+ { type: 'test', data: { foo: '1' } },
236
+ { type: 'test', data: { foo: '2' } },
237
+ ]);
238
+ await queue.fail((await queue.dequeue()), 'err');
239
+ await queue.fail((await queue.dequeue()), 'err');
240
+ // Breaker is Open. Wait for reset timeout (200ms)
241
+ await timeout(250);
242
+ const probe = await queue.dequeue();
243
+ expect(probe).toBeDefined();
244
+ const secondAttempt = await queue.dequeue();
245
+ expect(secondAttempt).toBeUndefined(); // Only 1 probe allowed in Half-Open
246
+ });
247
+ });
248
+ describe('Timeouts and Maintenance (Pruning)', () => {
249
+ it('should recover "Zombie" tasks (crashed workers)', async () => {
250
+ const task = await queue.enqueue('test', { foo: 'zombie' });
251
+ await queue.dequeue(); // Task is now Running with a token
252
+ // processTimeout is 200ms. Wait for it to expire.
253
+ await timeout(300);
254
+ await queue.maintenance();
255
+ const recovered = await queue.getTask(task.id);
256
+ expect(recovered?.status).toBe(TaskState.Pending);
257
+ expect(recovered?.tries).toBe(1);
258
+ expect(recovered?.token).toBeNull();
259
+ });
260
+ it('should fail tasks that exceed Hard Execution Timeout via prune', async () => {
261
+ // Re-configure queue with very short execution timeout
262
+ const queueProvider = injector.resolve(TaskQueueProvider);
263
+ const shortQueue = queueProvider.get(`prune-test-${Date.now()}`, { maxExecutionTime: 100 });
264
+ const task = await shortQueue.enqueue('test', { foo: 'long-running' });
265
+ await shortQueue.dequeue();
266
+ await timeout(200);
267
+ await shortQueue.maintenance();
268
+ const updated = await shortQueue.getTask(task.id);
269
+ expect(updated?.status).toBe(TaskState.Dead);
270
+ expect(updated?.error?.message).toContain('Hard Execution Timeout');
271
+ await shortQueue.clear();
272
+ });
273
+ it('should touch a task to extend token', async () => {
274
+ const task = await queue.enqueue('test', { foo: 'work' });
275
+ const dequeued = await queue.dequeue();
276
+ const initialLock = dequeued.visibilityDeadline;
277
+ await timeout(50);
278
+ const touched = await queue.touch(dequeued);
279
+ expect(touched?.visibilityDeadline).toBeGreaterThan(initialLock);
280
+ });
281
+ it('should prevent touching if token is lost (stolen by another worker)', async () => {
282
+ await queue.enqueue('test', { foo: 'work' });
283
+ const dequeued = await queue.dequeue();
284
+ expect(dequeued).toBeDefined();
285
+ // processTimeout is 200ms. Wait for it to expire.
286
+ await timeout(300);
287
+ await queue.maintenance();
288
+ await queue.dequeue(); // Stolen by another worker (tries=2)
289
+ // Original worker tries to touch
290
+ const touchResult = await queue.touch(dequeued);
291
+ expect(touchResult).toBeUndefined();
292
+ });
293
+ });
294
+ describe('Batch Operations', () => {
295
+ it('should complete many tasks efficiently', async () => {
296
+ const tasks = await queue.enqueueMany([
297
+ { type: 'test', data: { foo: '1' } },
298
+ { type: 'test', data: { foo: '2' } },
299
+ ], { returnTasks: true });
300
+ const d1 = await queue.dequeue();
301
+ const d2 = await queue.dequeue();
302
+ await queue.completeMany([d1, d2]);
303
+ const t1 = await queue.getTask(tasks[0].id);
304
+ const t2 = await queue.getTask(tasks[1].id);
305
+ expect(t1?.status).toBe(TaskState.Completed);
306
+ expect(t2?.status).toBe(TaskState.Completed);
307
+ });
308
+ });
309
+ describe('Rescheduling', () => {
310
+ it('should reschedule and refund tries if running', async () => {
311
+ const task = await queue.enqueue('test', { foo: 'reschedule-me' });
312
+ const dequeued = await queue.dequeue();
313
+ expect(dequeued?.tries).toBe(1);
314
+ const inFuture = currentTimestamp() + 1000;
315
+ await queue.reschedule(dequeued.id, inFuture);
316
+ const updated = await queue.getTask(task.id);
317
+ expect(updated?.status).toBe(TaskState.Pending);
318
+ expect(updated?.tries).toBe(0); // Refunded
319
+ expect(updated?.scheduleTimestamp).toBe(inFuture);
320
+ });
321
+ });
322
+ describe('TaskContext (Worker DX)', () => {
323
+ it('checkpoint() should update progress and handle token loss', async () => {
324
+ const task = await queue.enqueue('test', { foo: 'progress' });
325
+ const dequeued = await queue.dequeue();
326
+ // In real scenarios TaskContext wraps the queue logic.
327
+ // Here we just verify touch/checkpoint effects on the DB.
328
+ await queue.touch(dequeued, { progress: 0.5, state: { step: 1 } });
329
+ const updated = await queue.getTask(task.id);
330
+ expect(updated?.progress).toBe(0.5);
331
+ expect(updated?.state).toEqual({ step: 1 });
332
+ });
333
+ });
334
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,163 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
+ import { CancellationToken } from '../../cancellation/index.js';
3
+ import { TaskQueueProvider, TaskResult, TaskState } from '../../task-queue/index.js';
4
+ import { setupIntegrationTest } from '../../unit-test/index.js';
5
+ import { timeout } from '../../utils/timing.js';
6
+ describe('Worker & Base Class Tests', () => {
7
+ let injector;
8
+ let queue;
9
+ beforeAll(async () => {
10
+ ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
11
+ });
12
+ beforeEach(() => {
13
+ const queueProvider = injector.resolve(TaskQueueProvider);
14
+ const queueName = `worker-queue-${Date.now()}-${Math.random()}`;
15
+ queue = queueProvider.get(queueName, {
16
+ visibilityTimeout: 500, // Short visibility for testing lease loss
17
+ });
18
+ });
19
+ afterEach(async () => {
20
+ await queue.clear();
21
+ const queueProvider = injector.resolve(TaskQueueProvider);
22
+ await queueProvider.get('other-queue').clear();
23
+ });
24
+ afterAll(async () => {
25
+ await injector?.dispose();
26
+ });
27
+ it('should process tasks using process() helper', async () => {
28
+ const t1 = await queue.enqueue('work', { val: 1 });
29
+ const t2 = await queue.enqueue('work', { val: 2 });
30
+ const processed = [];
31
+ const token = new CancellationToken();
32
+ queue.process({ cancellationSignal: token }, async (context) => {
33
+ processed.push(context.data.val);
34
+ return TaskResult.Complete();
35
+ });
36
+ // Wait until 2 tasks are processed
37
+ for (let i = 0; i < 20; i++) {
38
+ if (processed.length === 2)
39
+ break;
40
+ await timeout(100);
41
+ }
42
+ token.set(); // Stop worker
43
+ expect(processed).toContain(1);
44
+ expect(processed).toContain(2);
45
+ expect(processed.length).toBe(2);
46
+ const check1 = await queue.getTask(t1.id);
47
+ const check2 = await queue.getTask(t2.id);
48
+ expect(check1?.status).toBe(TaskState.Completed);
49
+ expect(check2?.status).toBe(TaskState.Completed);
50
+ });
51
+ it('should handle errors in worker gracefully', async () => {
52
+ const task = await queue.enqueue('fail', {});
53
+ const token = new CancellationToken();
54
+ queue.process({ cancellationSignal: token }, async () => {
55
+ throw new Error('worker error');
56
+ });
57
+ await timeout(200);
58
+ token.set();
59
+ const updated = await queue.getTask(task.id);
60
+ expect(updated?.status).toBe(TaskState.Pending); // Should retry
61
+ expect(updated?.tries).toBe(1);
62
+ expect(updated?.error?.message).toBe('worker error');
63
+ });
64
+ it('should process batch of tasks', async () => {
65
+ await queue.enqueueMany([
66
+ { type: 'batch', data: { v: 1 } },
67
+ { type: 'batch', data: { v: 2 } },
68
+ { type: 'batch', data: { v: 3 } },
69
+ ]);
70
+ const processedBatch = [];
71
+ const token = new CancellationToken();
72
+ queue.processBatch({ batchSize: 2, cancellationSignal: token }, async (context) => {
73
+ expect(context.tasks.length).toBeLessThanOrEqual(2);
74
+ context.tasks.forEach(t => processedBatch.push(t.data.v));
75
+ return context.tasks.map(() => TaskResult.Complete());
76
+ });
77
+ for (let i = 0; i < 20; i++) {
78
+ if (processedBatch.length === 3)
79
+ break;
80
+ await timeout(100);
81
+ }
82
+ token.set();
83
+ expect(processedBatch.sort()).toEqual([1, 2, 3]);
84
+ });
85
+ it('should extend lease (heartbeat) during long processing', async () => {
86
+ const task = await queue.enqueue('long', {});
87
+ const token = new CancellationToken();
88
+ let executed = false;
89
+ queue.process({ cancellationSignal: token }, async (_context) => {
90
+ // Simulate long work > visibilityTimeout (500ms)
91
+ await timeout(700);
92
+ executed = true;
93
+ return TaskResult.Complete();
94
+ });
95
+ await timeout(1000);
96
+ token.set();
97
+ expect(executed).toBe(true);
98
+ const updated = await queue.getTask(task.id);
99
+ expect(updated?.status).toBe(TaskState.Completed);
100
+ });
101
+ it('should handle TaskResult actions (Fail, Reschedule)', async () => {
102
+ const tFail = await queue.enqueue('fail-action', {});
103
+ const tResched = await queue.enqueue('resched-action', {});
104
+ const token = new CancellationToken();
105
+ const processed = new Set();
106
+ queue.process({ cancellationSignal: token }, async (context) => {
107
+ processed.add(context.id);
108
+ if (context.id === tFail.id) {
109
+ return TaskResult.Fail(new Error('explicit fail'));
110
+ }
111
+ if (context.id === tResched.id) {
112
+ return TaskResult.RescheduleBy(1000);
113
+ }
114
+ return TaskResult.Complete();
115
+ });
116
+ for (let i = 0; i < 20; i++) {
117
+ if (processed.size === 2)
118
+ break;
119
+ await timeout(100);
120
+ }
121
+ token.set();
122
+ const uFail = await queue.getTask(tFail.id);
123
+ expect(uFail?.status).toBe(TaskState.Pending); // Retry
124
+ expect(uFail?.error?.message).toBe('explicit fail');
125
+ const uResched = await queue.getTask(tResched.id);
126
+ expect(uResched?.status).toBe(TaskState.Pending);
127
+ expect(uResched?.scheduleTimestamp).toBeGreaterThan(Date.now());
128
+ });
129
+ it('should exercise TaskContext methods', async () => {
130
+ const task = await queue.enqueue('context-test', { val: 1 });
131
+ const token = new CancellationToken();
132
+ let executed = false;
133
+ queue.process({ cancellationSignal: token, types: ['context-test'] }, async (context) => {
134
+ expect(context.id).toBe(task.id);
135
+ expect(context.data).toEqual({ val: 1 });
136
+ expect(context.attempt).toBe(1);
137
+ expect(context.triesLeft).toBeGreaterThan(0);
138
+ expect(context.logger).toBeDefined();
139
+ expect(context.signal).toBeDefined();
140
+ await context.checkpoint({ progress: 0.5 });
141
+ const child = await context.spawn('child', { c: 1 });
142
+ expect(child.parentId).toBe(task.id);
143
+ const children = await context.spawnMany([{ type: 'child', data: { c: 2 } }]);
144
+ expect(children[0]?.parentId).toBe(task.id);
145
+ // Other queue spawn
146
+ const otherQueue = injector.resolve(TaskQueueProvider).get('other-queue');
147
+ const otherChild = await context.spawn(otherQueue, 'other', { x: 1 });
148
+ expect(otherChild.parentId).toBe(task.id);
149
+ expect(otherChild.namespace).toBe('other-queue');
150
+ const otherChildren = await context.spawnMany(otherQueue, [{ type: 'other', data: { x: 2 } }]);
151
+ expect(otherChildren[0]?.parentId).toBe(task.id);
152
+ executed = true;
153
+ return TaskResult.Complete();
154
+ });
155
+ for (let i = 0; i < 20; i++) {
156
+ if (executed)
157
+ break;
158
+ await timeout(100);
159
+ }
160
+ token.set();
161
+ expect(executed).toBe(true);
162
+ });
163
+ });