@tstdl/base 0.93.182 → 0.93.183

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 (353) hide show
  1. package/api/server/api-request-token.provider.js +1 -1
  2. package/api/server/gateway.js +6 -1
  3. package/authentication/authentication.api.d.ts +13 -40
  4. package/authentication/authentication.api.js +5 -14
  5. package/authentication/client/authentication.service.d.ts +6 -14
  6. package/authentication/client/authentication.service.js +22 -4
  7. package/authentication/client/module.d.ts +1 -1
  8. package/authentication/client/module.js +4 -4
  9. package/authentication/models/index.d.ts +1 -0
  10. package/authentication/models/index.js +1 -0
  11. package/authentication/models/totp-results.model.d.ts +11 -0
  12. package/authentication/models/totp-results.model.js +37 -0
  13. package/authentication/server/authentication.api-controller.d.ts +3 -3
  14. package/authentication/server/authentication.api-controller.js +31 -4
  15. package/authentication/server/authentication.service.d.ts +5 -14
  16. package/authentication/server/authentication.service.js +6 -4
  17. package/core.d.ts +0 -5
  18. package/core.js +0 -8
  19. package/document-management/api/document-management.api.d.ts +2 -2
  20. package/document-management/service-models/document.service-model.d.ts +1 -1
  21. package/examples/config.d.ts +25 -0
  22. package/examples/config.js +26 -0
  23. package/notification/server/module.d.ts +1 -1
  24. package/notification/server/module.js +1 -1
  25. package/package.json +5 -5
  26. package/signals/api.d.ts +5 -1
  27. package/signals/api.js +3 -1
  28. package/signals/implementation/api.d.ts +13 -5
  29. package/signals/implementation/api.js +7 -1
  30. package/signals/implementation/asserts.d.ts +2 -2
  31. package/signals/implementation/asserts.js +3 -3
  32. package/signals/implementation/computed.d.ts +7 -34
  33. package/signals/implementation/computed.js +14 -83
  34. package/signals/implementation/configure.js +6 -2
  35. package/signals/implementation/effect.d.ts +65 -46
  36. package/signals/implementation/effect.js +97 -62
  37. package/signals/implementation/index.d.ts +2 -4
  38. package/signals/implementation/index.js +2 -4
  39. package/signals/implementation/linked_signal.d.ts +36 -0
  40. package/signals/implementation/linked_signal.js +34 -0
  41. package/signals/implementation/primitive/computed.d.ts +55 -0
  42. package/signals/implementation/primitive/computed.js +107 -0
  43. package/signals/implementation/primitive/effect.d.ts +26 -0
  44. package/signals/implementation/primitive/effect.js +31 -0
  45. package/signals/implementation/{equality.d.ts → primitive/equality.d.ts} +1 -1
  46. package/signals/implementation/{equality.js → primitive/equality.js} +1 -1
  47. package/signals/implementation/primitive/errors.d.ts +10 -0
  48. package/signals/implementation/{errors.js → primitive/errors.js} +3 -4
  49. package/signals/implementation/primitive/formatter.d.ts +19 -0
  50. package/signals/implementation/primitive/formatter.js +136 -0
  51. package/signals/implementation/{graph.d.ts → primitive/graph.d.ts} +68 -36
  52. package/signals/implementation/primitive/graph.js +386 -0
  53. package/signals/implementation/primitive/linked_signal.d.ts +46 -0
  54. package/signals/implementation/primitive/linked_signal.js +110 -0
  55. package/signals/implementation/primitive/signal.d.ts +31 -0
  56. package/signals/implementation/primitive/signal.js +80 -0
  57. package/signals/implementation/primitive/untracked.d.ts +12 -0
  58. package/signals/implementation/primitive/untracked.js +23 -0
  59. package/signals/implementation/{watch.d.ts → primitive/watch.d.ts} +1 -2
  60. package/signals/implementation/{watch.js → primitive/watch.js} +22 -16
  61. package/signals/implementation/resource/api.d.ts +275 -0
  62. package/signals/implementation/resource/api.js +26 -0
  63. package/signals/implementation/resource/debounce.d.ts +13 -0
  64. package/signals/implementation/resource/debounce.js +113 -0
  65. package/signals/implementation/resource/from_snapshots.d.ts +16 -0
  66. package/signals/implementation/resource/from_snapshots.js +44 -0
  67. package/signals/implementation/resource/index.d.ts +11 -0
  68. package/signals/implementation/resource/index.js +11 -0
  69. package/signals/implementation/resource/resource.d.ts +110 -0
  70. package/signals/implementation/resource/resource.js +402 -0
  71. package/signals/implementation/root_effect_scheduler.d.ts +50 -0
  72. package/signals/implementation/root_effect_scheduler.js +66 -0
  73. package/signals/implementation/signal.d.ts +42 -18
  74. package/signals/implementation/signal.js +29 -49
  75. package/signals/implementation/to-observable.d.ts +12 -5
  76. package/signals/implementation/to-observable.js +12 -2
  77. package/signals/implementation/to-signal.d.ts +9 -18
  78. package/signals/implementation/to-signal.js +46 -13
  79. package/signals/implementation/untracked.d.ts +1 -1
  80. package/signals/implementation/untracked.js +3 -11
  81. package/signals/operators/debounce.d.ts +8 -0
  82. package/signals/operators/debounce.js +19 -0
  83. package/signals/operators/derive-async.js +43 -15
  84. package/signals/operators/index.d.ts +2 -0
  85. package/signals/operators/index.js +2 -0
  86. package/signals/operators/throttle.d.ts +8 -0
  87. package/signals/operators/throttle.js +31 -0
  88. package/ai/genkit/tests/multi-region.test.d.ts +0 -2
  89. package/ai/genkit/tests/multi-region.test.js +0 -179
  90. package/ai/genkit/tests/token-limit-fallback.test.d.ts +0 -2
  91. package/ai/genkit/tests/token-limit-fallback.test.js +0 -209
  92. package/ai/prompts/tests/prompt-builder.test.d.ts +0 -1
  93. package/ai/prompts/tests/prompt-builder.test.js +0 -22
  94. package/ai/tests/instructions-formatter.test.d.ts +0 -1
  95. package/ai/tests/instructions-formatter.test.js +0 -116
  96. package/ai/tests/steering.test.d.ts +0 -1
  97. package/ai/tests/steering.test.js +0 -37
  98. package/api/client/tests/api-client.test.d.ts +0 -1
  99. package/api/client/tests/api-client.test.js +0 -194
  100. package/api/server/tests/csrf.middleware.test.d.ts +0 -1
  101. package/api/server/tests/csrf.middleware.test.js +0 -91
  102. package/authentication/tests/authentication-password-requirements.validator.test.d.ts +0 -1
  103. package/authentication/tests/authentication-password-requirements.validator.test.js +0 -29
  104. package/authentication/tests/authentication.api-controller.test.d.ts +0 -1
  105. package/authentication/tests/authentication.api-controller.test.js +0 -156
  106. package/authentication/tests/authentication.api-request-token.provider.test.d.ts +0 -1
  107. package/authentication/tests/authentication.api-request-token.provider.test.js +0 -48
  108. package/authentication/tests/authentication.client-error-handling.test.d.ts +0 -1
  109. package/authentication/tests/authentication.client-error-handling.test.js +0 -123
  110. package/authentication/tests/authentication.client-middleware.test.d.ts +0 -1
  111. package/authentication/tests/authentication.client-middleware.test.js +0 -118
  112. package/authentication/tests/authentication.client-service-methods.test.d.ts +0 -1
  113. package/authentication/tests/authentication.client-service-methods.test.js +0 -177
  114. package/authentication/tests/authentication.client-service-refresh.test.d.ts +0 -1
  115. package/authentication/tests/authentication.client-service-refresh.test.js +0 -153
  116. package/authentication/tests/authentication.client-service.test.d.ts +0 -1
  117. package/authentication/tests/authentication.client-service.test.js +0 -76
  118. package/authentication/tests/authentication.refresh-busy-loop.test.d.ts +0 -1
  119. package/authentication/tests/authentication.refresh-busy-loop.test.js +0 -84
  120. package/authentication/tests/authentication.service.test.d.ts +0 -1
  121. package/authentication/tests/authentication.service.test.js +0 -167
  122. package/authentication/tests/authentication.test-ancillary-service.d.ts +0 -9
  123. package/authentication/tests/authentication.test-ancillary-service.js +0 -27
  124. package/authentication/tests/brute-force-protection.test.d.ts +0 -1
  125. package/authentication/tests/brute-force-protection.test.js +0 -211
  126. package/authentication/tests/helper.test.d.ts +0 -1
  127. package/authentication/tests/helper.test.js +0 -122
  128. package/authentication/tests/password-requirements.error.test.d.ts +0 -1
  129. package/authentication/tests/password-requirements.error.test.js +0 -14
  130. package/authentication/tests/remember.api.test.d.ts +0 -1
  131. package/authentication/tests/remember.api.test.js +0 -117
  132. package/authentication/tests/remember.service.test.d.ts +0 -1
  133. package/authentication/tests/remember.service.test.js +0 -83
  134. package/authentication/tests/subject.service.test.d.ts +0 -1
  135. package/authentication/tests/subject.service.test.js +0 -140
  136. package/authentication/tests/suspended-subject.test.d.ts +0 -1
  137. package/authentication/tests/suspended-subject.test.js +0 -120
  138. package/authentication/tests/totp.enrollment.test.d.ts +0 -1
  139. package/authentication/tests/totp.enrollment.test.js +0 -123
  140. package/authentication/tests/totp.login.test.d.ts +0 -1
  141. package/authentication/tests/totp.login.test.js +0 -213
  142. package/authentication/tests/totp.recovery-codes.test.d.ts +0 -1
  143. package/authentication/tests/totp.recovery-codes.test.js +0 -97
  144. package/authentication/tests/totp.status.test.d.ts +0 -1
  145. package/authentication/tests/totp.status.test.js +0 -72
  146. package/cancellation/tests/coverage.test.d.ts +0 -1
  147. package/cancellation/tests/coverage.test.js +0 -49
  148. package/cancellation/tests/leak.test.d.ts +0 -1
  149. package/cancellation/tests/leak.test.js +0 -35
  150. package/cancellation/tests/token.test.d.ts +0 -1
  151. package/cancellation/tests/token.test.js +0 -136
  152. package/circuit-breaker/tests/circuit-breaker.test.d.ts +0 -1
  153. package/circuit-breaker/tests/circuit-breaker.test.js +0 -116
  154. package/cryptography/tests/cryptography.test.d.ts +0 -1
  155. package/cryptography/tests/cryptography.test.js +0 -175
  156. package/cryptography/tests/jwt.test.d.ts +0 -1
  157. package/cryptography/tests/jwt.test.js +0 -54
  158. package/cryptography/tests/modern.test.d.ts +0 -1
  159. package/cryptography/tests/modern.test.js +0 -105
  160. package/cryptography/tests/module.test.d.ts +0 -1
  161. package/cryptography/tests/module.test.js +0 -100
  162. package/cryptography/tests/totp.test.d.ts +0 -1
  163. package/cryptography/tests/totp.test.js +0 -108
  164. package/document-management/tests/ai-config-hierarchy.test.d.ts +0 -1
  165. package/document-management/tests/ai-config-hierarchy.test.js +0 -59
  166. package/document-management/tests/ai-config-integration.test.d.ts +0 -1
  167. package/document-management/tests/ai-config-integration.test.js +0 -125
  168. package/document-management/tests/ai-config-merge.test.d.ts +0 -1
  169. package/document-management/tests/ai-config-merge.test.js +0 -46
  170. package/document-management/tests/document-management-ai-overrides.test.d.ts +0 -1
  171. package/document-management/tests/document-management-ai-overrides.test.js +0 -63
  172. package/document-management/tests/document-management-core.test.d.ts +0 -1
  173. package/document-management/tests/document-management-core.test.js +0 -157
  174. package/document-management/tests/document-management.api.test.d.ts +0 -1
  175. package/document-management/tests/document-management.api.test.js +0 -101
  176. package/document-management/tests/document-statistics.service.test.d.ts +0 -1
  177. package/document-management/tests/document-statistics.service.test.js +0 -498
  178. package/document-management/tests/document-validation-ai-overrides.test.d.ts +0 -1
  179. package/document-management/tests/document-validation-ai-overrides.test.js +0 -87
  180. package/document-management/tests/document.service.test.d.ts +0 -1
  181. package/document-management/tests/document.service.test.js +0 -143
  182. package/document-management/tests/enum-helpers.test.d.ts +0 -1
  183. package/document-management/tests/enum-helpers.test.js +0 -452
  184. package/document-management/tests/helper.d.ts +0 -24
  185. package/document-management/tests/helper.js +0 -39
  186. package/errors/tests/format.test.d.ts +0 -1
  187. package/errors/tests/format.test.js +0 -84
  188. package/http/tests/server-timing.test.d.ts +0 -1
  189. package/http/tests/server-timing.test.js +0 -42
  190. package/injector/tests/advanced.test.d.ts +0 -1
  191. package/injector/tests/advanced.test.js +0 -116
  192. package/injector/tests/async-init.test.d.ts +0 -1
  193. package/injector/tests/async-init.test.js +0 -77
  194. package/injector/tests/basic.test.d.ts +0 -1
  195. package/injector/tests/basic.test.js +0 -114
  196. package/injector/tests/hierarchical.test.d.ts +0 -1
  197. package/injector/tests/hierarchical.test.js +0 -59
  198. package/injector/tests/leak.test.d.ts +0 -1
  199. package/injector/tests/leak.test.js +0 -45
  200. package/injector/tests/lifecycles.test.d.ts +0 -1
  201. package/injector/tests/lifecycles.test.js +0 -109
  202. package/logger/tests/pretty-print.test.d.ts +0 -1
  203. package/logger/tests/pretty-print.test.js +0 -60
  204. package/notification/tests/notification-api.test.d.ts +0 -1
  205. package/notification/tests/notification-api.test.js +0 -124
  206. package/notification/tests/notification-client.test.d.ts +0 -1
  207. package/notification/tests/notification-client.test.js +0 -101
  208. package/notification/tests/notification-flow.test.d.ts +0 -1
  209. package/notification/tests/notification-flow.test.js +0 -296
  210. package/notification/tests/notification-sse.service.test.d.ts +0 -1
  211. package/notification/tests/notification-sse.service.test.js +0 -43
  212. package/notification/tests/notification-type.service.test.d.ts +0 -1
  213. package/notification/tests/notification-type.service.test.js +0 -41
  214. package/object-storage/s3/tests/s3.object-storage.integration.test.d.ts +0 -1
  215. package/object-storage/s3/tests/s3.object-storage.integration.test.js +0 -303
  216. package/orm/tests/build-jsonb.test.d.ts +0 -1
  217. package/orm/tests/build-jsonb.test.js +0 -39
  218. package/orm/tests/data-types.test.d.ts +0 -1
  219. package/orm/tests/data-types.test.js +0 -39
  220. package/orm/tests/database-extension.test.d.ts +0 -1
  221. package/orm/tests/database-extension.test.js +0 -63
  222. package/orm/tests/database-migration.test.d.ts +0 -1
  223. package/orm/tests/database-migration.test.js +0 -83
  224. package/orm/tests/decorators.test.d.ts +0 -1
  225. package/orm/tests/decorators.test.js +0 -77
  226. package/orm/tests/encryption.test.d.ts +0 -1
  227. package/orm/tests/encryption.test.js +0 -31
  228. package/orm/tests/query-complex.test.d.ts +0 -1
  229. package/orm/tests/query-complex.test.js +0 -172
  230. package/orm/tests/query-converter-complex.test.d.ts +0 -1
  231. package/orm/tests/query-converter-complex.test.js +0 -131
  232. package/orm/tests/query-converter.test.d.ts +0 -1
  233. package/orm/tests/query-converter.test.js +0 -123
  234. package/orm/tests/repository-advanced.test.d.ts +0 -1
  235. package/orm/tests/repository-advanced.test.js +0 -189
  236. package/orm/tests/repository-attributes.test.d.ts +0 -1
  237. package/orm/tests/repository-attributes.test.js +0 -83
  238. package/orm/tests/repository-compound-primary-key.test.d.ts +0 -2
  239. package/orm/tests/repository-compound-primary-key.test.js +0 -226
  240. package/orm/tests/repository-comprehensive.test.d.ts +0 -1
  241. package/orm/tests/repository-comprehensive.test.js +0 -162
  242. package/orm/tests/repository-coverage.test.d.ts +0 -2
  243. package/orm/tests/repository-coverage.test.js +0 -242
  244. package/orm/tests/repository-cti-complex.test.d.ts +0 -1
  245. package/orm/tests/repository-cti-complex.test.js +0 -151
  246. package/orm/tests/repository-cti-embedded.test.d.ts +0 -1
  247. package/orm/tests/repository-cti-embedded.test.js +0 -178
  248. package/orm/tests/repository-cti-extensive.test.d.ts +0 -2
  249. package/orm/tests/repository-cti-extensive.test.js +0 -279
  250. package/orm/tests/repository-cti-mapping.test.d.ts +0 -2
  251. package/orm/tests/repository-cti-mapping.test.js +0 -108
  252. package/orm/tests/repository-cti-search.test.d.ts +0 -1
  253. package/orm/tests/repository-cti-search.test.js +0 -141
  254. package/orm/tests/repository-cti-soft-delete.test.d.ts +0 -2
  255. package/orm/tests/repository-cti-soft-delete.test.js +0 -103
  256. package/orm/tests/repository-cti-transactions.test.d.ts +0 -1
  257. package/orm/tests/repository-cti-transactions.test.js +0 -112
  258. package/orm/tests/repository-cti-upsert-many.test.d.ts +0 -2
  259. package/orm/tests/repository-cti-upsert-many.test.js +0 -115
  260. package/orm/tests/repository-cti.test.d.ts +0 -2
  261. package/orm/tests/repository-cti.test.js +0 -390
  262. package/orm/tests/repository-edge-cases.test.d.ts +0 -1
  263. package/orm/tests/repository-edge-cases.test.js +0 -178
  264. package/orm/tests/repository-expiration.test.d.ts +0 -2
  265. package/orm/tests/repository-expiration.test.js +0 -140
  266. package/orm/tests/repository-extra-coverage.test.d.ts +0 -2
  267. package/orm/tests/repository-extra-coverage.test.js +0 -402
  268. package/orm/tests/repository-mapping.test.d.ts +0 -2
  269. package/orm/tests/repository-mapping.test.js +0 -65
  270. package/orm/tests/repository-regression.test.d.ts +0 -1
  271. package/orm/tests/repository-regression.test.js +0 -288
  272. package/orm/tests/repository-search-coverage.test.d.ts +0 -1
  273. package/orm/tests/repository-search-coverage.test.js +0 -107
  274. package/orm/tests/repository-search.test.d.ts +0 -1
  275. package/orm/tests/repository-search.test.js +0 -105
  276. package/orm/tests/repository-soft-delete.test.d.ts +0 -1
  277. package/orm/tests/repository-soft-delete.test.js +0 -118
  278. package/orm/tests/repository-transactions-nested.test.d.ts +0 -1
  279. package/orm/tests/repository-transactions-nested.test.js +0 -178
  280. package/orm/tests/repository-types.test.d.ts +0 -1
  281. package/orm/tests/repository-types.test.js +0 -184
  282. package/orm/tests/repository-undelete.test.d.ts +0 -2
  283. package/orm/tests/repository-undelete.test.js +0 -201
  284. package/orm/tests/schema-converter.test.d.ts +0 -1
  285. package/orm/tests/schema-converter.test.js +0 -82
  286. package/orm/tests/schema-generation.test.d.ts +0 -2
  287. package/orm/tests/schema-generation.test.js +0 -174
  288. package/orm/tests/sql-helpers.test.d.ts +0 -1
  289. package/orm/tests/sql-helpers.test.js +0 -67
  290. package/orm/tests/transaction-safety.test.d.ts +0 -1
  291. package/orm/tests/transaction-safety.test.js +0 -81
  292. package/orm/tests/transactional.test.d.ts +0 -1
  293. package/orm/tests/transactional.test.js +0 -215
  294. package/orm/tests/utils.test.d.ts +0 -1
  295. package/orm/tests/utils.test.js +0 -70
  296. package/pdf/tests/utils.test.d.ts +0 -1
  297. package/pdf/tests/utils.test.js +0 -187
  298. package/process/tests/spawn.test.d.ts +0 -1
  299. package/process/tests/spawn.test.js +0 -182
  300. package/rate-limit/tests/postgres-rate-limiter.test.d.ts +0 -1
  301. package/rate-limit/tests/postgres-rate-limiter.test.js +0 -84
  302. package/renderer/tests/renderer.test.d.ts +0 -1
  303. package/renderer/tests/renderer.test.js +0 -88
  304. package/rpc/tests/rpc.integration.test.d.ts +0 -1
  305. package/rpc/tests/rpc.integration.test.js +0 -615
  306. package/signals/implementation/errors.d.ts +0 -2
  307. package/signals/implementation/graph.js +0 -312
  308. package/signals/implementation/writable-signal.d.ts +0 -48
  309. package/signals/implementation/writable-signal.js +0 -32
  310. package/task-queue/tests/coverage-branch.test.d.ts +0 -1
  311. package/task-queue/tests/coverage-branch.test.js +0 -395
  312. package/task-queue/tests/coverage-enhancement.test.d.ts +0 -1
  313. package/task-queue/tests/coverage-enhancement.test.js +0 -150
  314. package/task-queue/tests/dag.test.d.ts +0 -1
  315. package/task-queue/tests/dag.test.js +0 -188
  316. package/task-queue/tests/dependencies.test.d.ts +0 -1
  317. package/task-queue/tests/dependencies.test.js +0 -296
  318. package/task-queue/tests/enqueue-batch.test.d.ts +0 -1
  319. package/task-queue/tests/enqueue-batch.test.js +0 -125
  320. package/task-queue/tests/enqueue-item.test.d.ts +0 -1
  321. package/task-queue/tests/enqueue-item.test.js +0 -12
  322. package/task-queue/tests/fan-out-spawning.test.d.ts +0 -1
  323. package/task-queue/tests/fan-out-spawning.test.js +0 -94
  324. package/task-queue/tests/idempotent-replacement.test.d.ts +0 -1
  325. package/task-queue/tests/idempotent-replacement.test.js +0 -114
  326. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +0 -1
  327. package/task-queue/tests/missing-idempotent-tasks.test.js +0 -39
  328. package/task-queue/tests/optimization-edge-cases.test.d.ts +0 -1
  329. package/task-queue/tests/optimization-edge-cases.test.js +0 -124
  330. package/task-queue/tests/queue-generic.test.d.ts +0 -1
  331. package/task-queue/tests/queue-generic.test.js +0 -8
  332. package/task-queue/tests/queue.test.d.ts +0 -1
  333. package/task-queue/tests/queue.test.js +0 -756
  334. package/task-queue/tests/shutdown.test.d.ts +0 -1
  335. package/task-queue/tests/shutdown.test.js +0 -41
  336. package/task-queue/tests/task-context.test.d.ts +0 -1
  337. package/task-queue/tests/task-context.test.js +0 -7
  338. package/task-queue/tests/task-union.test.d.ts +0 -1
  339. package/task-queue/tests/task-union.test.js +0 -18
  340. package/task-queue/tests/transactions.test.d.ts +0 -1
  341. package/task-queue/tests/transactions.test.js +0 -47
  342. package/task-queue/tests/typing.test.d.ts +0 -1
  343. package/task-queue/tests/typing.test.js +0 -9
  344. package/task-queue/tests/worker.test.d.ts +0 -1
  345. package/task-queue/tests/worker.test.js +0 -258
  346. package/task-queue/tests/zombie-parent.test.d.ts +0 -1
  347. package/task-queue/tests/zombie-parent.test.js +0 -45
  348. package/task-queue/tests/zombie-recovery.test.d.ts +0 -1
  349. package/task-queue/tests/zombie-recovery.test.js +0 -51
  350. package/utils/tests/backoff.test.d.ts +0 -1
  351. package/utils/tests/backoff.test.js +0 -41
  352. package/utils/tests/retry-with-backoff.test.d.ts +0 -1
  353. package/utils/tests/retry-with-backoff.test.js +0 -49
@@ -1,756 +0,0 @@
1
- import { eq, or } from 'drizzle-orm';
2
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
3
- import { CancellationToken } from '../../cancellation/index.js';
4
- import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
5
- import { Database } from '../../orm/server/index.js';
6
- import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
7
- import { task as taskTable } from '../../task-queue/postgres/schemas.js';
8
- import { setupIntegrationTest } from '../../testing/index.js';
9
- import { currentTimestamp } from '../../utils/date-time.js';
10
- import { Timer } from '../../utils/timer.js';
11
- import { timeout } from '../../utils/timing.js';
12
- import { isDefined } from '../../utils/type-guards.js';
13
- describe('Queue Integration Tests', () => {
14
- let injector;
15
- let queue;
16
- // Helper to verify state in DB
17
- async function assertTaskStatus(id, state, message) {
18
- const task = await queue.getTask(id);
19
- expect(task?.status, message).toBe(state);
20
- }
21
- beforeAll(async () => {
22
- ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
23
- });
24
- beforeEach(async () => {
25
- const queueProvider = injector.resolve(TaskQueueProvider);
26
- const queueName = `test-queue-${crypto.randomUUID()}`;
27
- queue = queueProvider.get(queueName, {
28
- visibilityTimeout: 5000,
29
- retryDelayMinimum: 100, // Fast retries for testing
30
- retryDelayGrowth: 1, // Linear/No growth for predictable tests
31
- });
32
- });
33
- afterEach(async () => {
34
- // Drain the queue to prevent state leakage between tests
35
- await queue.clear();
36
- });
37
- afterAll(async () => {
38
- try {
39
- if (isDefined(queue)) {
40
- await queue.clear();
41
- }
42
- await injector.dispose();
43
- }
44
- catch (error) {
45
- // Ignore known double-dispose issue from MessageBus interaction
46
- if (error instanceof Error && error.message === 'MessageBus is disposed.') {
47
- return;
48
- }
49
- throw error;
50
- }
51
- });
52
- it('Basic FIFO Flow', async () => {
53
- const t1 = await queue.enqueue('test', { value: 'first' });
54
- const t2 = await queue.enqueue('test', { value: 'second' });
55
- const d1 = await queue.dequeue();
56
- expect(d1?.id).toBe(t1.id);
57
- expect((d1?.data)['value']).toBe('first');
58
- await queue.complete(d1, { result: { success: true } });
59
- await assertTaskStatus(t1.id, TaskStatus.Completed, 'Task 1 completed');
60
- const d2 = await queue.dequeue();
61
- expect(d2?.id).toBe(t2.id);
62
- await queue.complete(d2, { result: { success: true } });
63
- });
64
- it('Priorities', async () => {
65
- // Priority 1000 (default)
66
- const low = await queue.enqueue('test', { value: 'low' });
67
- // Priority 1
68
- const high = await queue.enqueue('test', { value: 'high' }, { priority: 1 });
69
- const first = await queue.dequeue();
70
- expect(first?.id).toBe(high.id);
71
- await queue.complete(first);
72
- const second = await queue.dequeue();
73
- expect(second?.id).toBe(low.id);
74
- await queue.complete(second);
75
- });
76
- it('Deduplication (Idempotency Keys)', async () => {
77
- const key = `unique-${crypto.randomUUID()}`;
78
- // 1. Initial Insert
79
- const t1 = await queue.enqueue('test', { value: 'original' }, { idempotencyKey: key });
80
- // 2. Default Strategy (replace: false): Should return existing task, ignore new data
81
- const t2 = await queue.enqueue('test', { value: 'ignored' }, { idempotencyKey: key });
82
- expect(t2.id, 'Same ID if not replaced').toBe(t1.id);
83
- const check1 = await queue.getTask(t1.id);
84
- expect((check1?.data)['value']).toBe('original');
85
- // 3. Replace Strategy: Should replace existing task with new data. ID stays the same to avoid foreign key violations.
86
- const t3 = await queue.enqueueMany([{ type: 'test', data: { value: 'updated' }, idempotencyKey: key }], { replace: true, returnTasks: true });
87
- expect(t3[0].id, 'Same ID if replaced').toBe(t1.id);
88
- // New task should have new data
89
- const checkNew = await queue.getTask(t3[0].id);
90
- expect((checkNew?.data)['value']).toBe('updated');
91
- expect(checkNew?.tries).toBe(0);
92
- });
93
- it('Retries and Failures', async () => {
94
- const task = await queue.enqueue('test', { value: 'fail-me' });
95
- // Try 1
96
- const attempt1 = await queue.dequeue();
97
- expect(attempt1?.id).toBe(task.id);
98
- await queue.fail(attempt1, { message: 'oops' });
99
- // Verify Retrying status
100
- await assertTaskStatus(task.id, TaskStatus.Retrying, 'Task enters Retrying status after failure');
101
- // Force reschedule to now to bypass retryDelay
102
- await queue.reschedule(task.id, currentTimestamp());
103
- // Try 2
104
- const attempt2 = await queue.dequeue();
105
- expect(attempt2?.id).toBe(task.id);
106
- expect(attempt2?.tries).toBe(2);
107
- // Fail fatally
108
- await queue.fail(attempt2, { message: 'fatal error' }, { fatal: true });
109
- await assertTaskStatus(task.id, TaskStatus.Dead, 'Task is Dead after fatal error');
110
- });
111
- it('Hierarchy (Parent/Child)', async () => {
112
- // A. Create Parent
113
- const p = await queue.enqueue('test', { value: 'parent-manual' });
114
- // B. Dequeue Parent
115
- const pTask = await queue.dequeue();
116
- expect(pTask?.id).toBe(p.id);
117
- // C. Parent spawns child
118
- const child = await queue.enqueue('test', { value: 'child-manual' }, { parentId: p.id });
119
- // D. "Finish" Parent execution.
120
- await queue.complete(pTask);
121
- // await assertTaskStatus(p.id, TaskStatus.Waiting, 'Parent entered WAITING state'); // Depends on implementation details of auto-waiting
122
- });
123
- it('Batching', async () => {
124
- const batch = queue.batch();
125
- for (let i = 0; i < 5; i++) {
126
- batch.add('test', { value: `batch-${i}` });
127
- }
128
- const tasks = await batch.enqueue({ returnTasks: true });
129
- expect(tasks.length).toBe(5);
130
- const dequeuedBatch = await queue.dequeueMany(5);
131
- expect(dequeuedBatch.length).toBe(5);
132
- await queue.completeMany(dequeuedBatch);
133
- const leftover = await queue.dequeue();
134
- expect(leftover).toBeUndefined();
135
- });
136
- });
137
- describe('PostgresQueue (Distributed Task Orchestration)', () => {
138
- let injector;
139
- let queue;
140
- let database;
141
- beforeAll(async () => {
142
- ({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
143
- database = injector.resolve(Database);
144
- });
145
- beforeEach(() => {
146
- const queueProvider = injector.resolve(TaskQueueProvider);
147
- const queueName = `pg-test-queue-${crypto.randomUUID()}`;
148
- queue = queueProvider.get(queueName, {
149
- visibilityTimeout: 50, // Short timeout for testing
150
- retryDelayMinimum: 50,
151
- retryDelayGrowth: 1,
152
- circuitBreakerThreshold: 2,
153
- circuitBreakerResetTimeout: 50,
154
- });
155
- });
156
- afterEach(async () => {
157
- await queue.clear();
158
- });
159
- afterAll(async () => {
160
- await injector?.dispose();
161
- });
162
- describe('Basic Lifecycle', () => {
163
- it('should enqueue and dequeue a task', async () => {
164
- await queue.enqueue('foo', { foo: 'bar' });
165
- const task = await queue.dequeue();
166
- expect(task).toBeDefined();
167
- expect(task?.data).toEqual({ foo: 'bar' });
168
- expect(task?.status).toBe(TaskStatus.Running);
169
- expect(task?.tries).toBe(1);
170
- });
171
- it('should complete a task successfully', async () => {
172
- const task = await queue.enqueue('foo', { foo: 'bar' });
173
- const dequeued = await queue.dequeue();
174
- await queue.complete(dequeued, { result: { result: true } });
175
- const updated = await queue.getTask(task.id);
176
- expect(updated?.status).toBe(TaskStatus.Completed);
177
- expect(updated?.result).toEqual({ result: true });
178
- expect(updated.completeTimestamp > 0).toBe(true);
179
- });
180
- it('should fail a task and increment tries', async () => {
181
- const task = await queue.enqueue('foo', { foo: 'bar' });
182
- const dequeued = await queue.dequeue();
183
- await queue.fail(dequeued, new Error('temp failure'));
184
- const updated = await queue.getTask(task.id);
185
- expect(updated?.status).toBe(TaskStatus.Retrying);
186
- expect(updated?.tries).toBe(1);
187
- expect(updated?.errors).toHaveLength(1);
188
- });
189
- it('should NOT clear startTimestamp when transitioning to terminal or retry states', async () => {
190
- // Re-create queue with high circuit breaker threshold for this test
191
- const q = injector.resolve(TaskQueueProvider).get(`start-timestamp-${crypto.randomUUID()}`, {
192
- circuitBreakerThreshold: 10,
193
- });
194
- const task = await q.enqueue('foo', { foo: 'bar' });
195
- // 1. Dequeue to start it (sets startTimestamp)
196
- const dequeued = await q.dequeue();
197
- expect(dequeued?.startTimestamp).not.toBeNull();
198
- const startTimestamp = dequeued.startTimestamp;
199
- // 2. Fail it (transitions to Retrying)
200
- await q.fail(dequeued, new Error('temp failure'));
201
- const retryingTask = await q.getTask(task.id);
202
- expect(retryingTask?.status).toBe(TaskStatus.Retrying);
203
- expect(retryingTask?.startTimestamp).toBe(startTimestamp);
204
- // 3. Dequeue again (updates startTimestamp)
205
- await q.reschedule(task.id, currentTimestamp());
206
- const dequeued2 = await q.dequeue();
207
- expect(dequeued2?.startTimestamp).not.toBe(startTimestamp);
208
- const startTimestamp2 = dequeued2.startTimestamp;
209
- // 4. Fail fatally (transitions to Dead)
210
- await q.fail(dequeued2, new Error('fatal failure'), { fatal: true });
211
- const deadTask = await q.getTask(task.id);
212
- expect(deadTask?.status).toBe(TaskStatus.Dead);
213
- expect(deadTask?.startTimestamp).toBe(startTimestamp2);
214
- // 5. Success (transitions to Completed)
215
- const task2 = await q.enqueue('foo', { foo: 'bar2' });
216
- const dequeued3 = await q.dequeue();
217
- expect(dequeued3).toBeDefined();
218
- const startTimestamp3 = dequeued3.startTimestamp;
219
- await q.complete(dequeued3);
220
- const completedTask = await q.getTask(task2.id);
221
- expect(completedTask?.status).toBe(TaskStatus.Completed);
222
- expect(completedTask?.startTimestamp).toBe(startTimestamp3);
223
- await q.clear();
224
- });
225
- });
226
- describe('Hierarchy and Cross-Namespace', () => {
227
- it('should correctly increment parent unresolved dependencies when a child is spawned in a different namespace', async () => {
228
- const queueProvider = injector.resolve(TaskQueueProvider);
229
- const nameA = `QueueA-${crypto.randomUUID()}`;
230
- const nameB = `QueueB-${crypto.randomUUID()}`;
231
- const queueA = queueProvider.get(nameA);
232
- const queueB = queueProvider.get(nameB);
233
- const parent = await queueA.enqueue('test', { value: 'parent' });
234
- expect(parent.unresolvedCompleteDependencies).toBe(0);
235
- await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id, parentRequires: true });
236
- const updatedParent = await queueA.getTask(parent.id);
237
- expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
238
- await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
239
- await queueB.clear();
240
- await queueA.clear();
241
- });
242
- it('should recursively cancel the entire task tree spanning multiple namespaces', async () => {
243
- const queueProvider = injector.resolve(TaskQueueProvider);
244
- const nameA = `NamespaceA-${crypto.randomUUID()}`;
245
- const nameB = `NamespaceB-${crypto.randomUUID()}`;
246
- const queueA = queueProvider.get(nameA);
247
- const queueB = queueProvider.get(nameB);
248
- const parent = await queueA.enqueue('test', { value: 'parent' });
249
- const child = await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id });
250
- const grandchild = await queueA.enqueue('test', { value: 'grandchild' }, { parentId: child.id });
251
- await queueA.cancel(parent.id);
252
- await queueB.waitForTasks([child.id], { statuses: [TaskStatus.Cancelled] });
253
- await queueA.waitForTasks([grandchild.id], { statuses: [TaskStatus.Cancelled] });
254
- const updatedChild = await queueB.getTask(child.id);
255
- const updatedGrandchild = await queueA.getTask(grandchild.id);
256
- expect(updatedChild?.status).toBe(TaskStatus.Cancelled);
257
- expect(updatedGrandchild?.status).toBe(TaskStatus.Cancelled);
258
- await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
259
- await queueB.clear();
260
- await queueA.clear();
261
- });
262
- it('should successfully cancel tasks from other namespaces (Bug 4)', async () => {
263
- const queueProvider = injector.resolve(TaskQueueProvider);
264
- const nameA = `QueueA-${crypto.randomUUID()}`;
265
- const nameB = `QueueB-${crypto.randomUUID()}`;
266
- const queueA = queueProvider.get(nameA);
267
- const queueB = queueProvider.get(nameB);
268
- // 1. Enqueue task in queueB
269
- const taskB = await queueB.enqueue('test', {});
270
- expect(taskB.status).toBe(TaskStatus.Pending);
271
- // 2. Cancel task in queueA (using taskB.id which is from other namespace)
272
- await queueA.cancel(taskB.id);
273
- // 3. Verify taskB IS cancelled (because ID is global)
274
- const updatedB = await queueB.getTask(taskB.id);
275
- expect(updatedB?.status).toBe(TaskStatus.Cancelled);
276
- await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
277
- await queueB.clear();
278
- await queueA.clear();
279
- });
280
- it('should complete parent task if idempotent child is already completed (Bug 6)', async () => {
281
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
282
- // 1. Enqueue and complete child task with idempotency key
283
- const idempotencyKey = 'child-idempotency-key';
284
- const child = await q.enqueue('test', {}, { idempotencyKey });
285
- const dequeuedChild = await q.dequeue();
286
- await q.complete(dequeuedChild);
287
- const finishedChild = await q.getTask(child.id);
288
- expect(finishedChild?.status).toBe(TaskStatus.Completed);
289
- // 2. Enqueue parent task that spawns the same child (via idempotency key)
290
- const parent = await q.enqueue('test', {});
291
- await q.enqueue('test', {}, {
292
- idempotencyKey,
293
- parentId: parent.id,
294
- parentRequires: true,
295
- });
296
- // Dequeue and complete parent
297
- const dequeuedParent = await q.dequeue();
298
- await q.complete(dequeuedParent);
299
- const updatedParent = await q.getTask(parent.id);
300
- expect(updatedParent?.status).toBe(TaskStatus.Completed);
301
- await q.clear();
302
- });
303
- it('should increment unresolvedCompleteDependencies for children with parentRequires: true (Bug 6-2)', async () => {
304
- const parent = await queue.enqueue('parent', {});
305
- expect(parent.unresolvedCompleteDependencies).toBe(0);
306
- await queue.enqueue('child', {}, { parentId: parent.id, parentRequires: true });
307
- const updatedParent = await queue.getTask(parent.id);
308
- expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
309
- });
310
- });
311
- describe('Priority and Scheduling', () => {
312
- it('should dequeue tasks in priority order (lower number first)', async () => {
313
- await queue.enqueue('foo', { foo: 'low' }, { priority: 2000 });
314
- await queue.enqueue('foo', { foo: 'high' }, { priority: 10 });
315
- await queue.enqueue('foo', { foo: 'mid' }, { priority: 1000 });
316
- const t1 = await queue.dequeue();
317
- const t2 = await queue.dequeue();
318
- const t3 = await queue.dequeue();
319
- expect((t1?.data)['foo']).toBe('high');
320
- expect((t2?.data)['foo']).toBe('mid');
321
- expect((t3?.data)['foo']).toBe('low');
322
- });
323
- it('should not dequeue a task scheduled in the future', async () => {
324
- const future = currentTimestamp() + 100;
325
- await queue.enqueue('foo', { foo: 'future' }, { scheduleTimestamp: future });
326
- const task = await queue.dequeue();
327
- expect(task).toBeUndefined();
328
- await timeout(150);
329
- const taskLater = await queue.dequeue();
330
- expect(taskLater).toBeDefined();
331
- });
332
- });
333
- describe('Concurrency Control', () => {
334
- it('should refund the rate limiter tokens when dequeueMany retrieves fewer tasks than requested', async () => {
335
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, {
336
- rateLimit: 5,
337
- rateInterval: 60000,
338
- });
339
- await q.enqueue('test', { value: '1' });
340
- const tasks1 = await q.dequeueMany(5); // Consumes 5, gets 1, refunds 4.
341
- expect(tasks1).toHaveLength(1);
342
- await q.enqueue('test', { value: '2' });
343
- const tasks2 = await q.dequeueMany(4); // Should be allowed because of refund.
344
- expect(tasks2).toHaveLength(1);
345
- await q.clear();
346
- });
347
- });
348
- describe('Circuit Breaker', () => {
349
- it('should trip the breaker after threshold failures', async () => {
350
- // Config: circuitBreakerThreshold: 2 (set in beforeEach)
351
- await queue.enqueue('foo', { foo: '1' });
352
- await queue.enqueue('foo', { foo: '2' });
353
- await queue.enqueue('foo', { foo: '3' });
354
- await queue.fail((await queue.dequeue()), 'err');
355
- await queue.fail((await queue.dequeue()), 'err');
356
- // Breaker should be Open
357
- const t3Attempt = await queue.dequeue();
358
- expect(t3Attempt).toBeUndefined();
359
- });
360
- it('should only allow a single active task in Half-Open state', async () => {
361
- await queue.enqueueMany([
362
- { type: 'foo', data: { foo: '1' } },
363
- { type: 'foo', data: { foo: '2' } },
364
- ]);
365
- await queue.fail((await queue.dequeue()), 'err');
366
- await queue.fail((await queue.dequeue()), 'err');
367
- // Breaker is Open. Wait for reset timeout (50ms)
368
- await timeout(75);
369
- const probe = await queue.dequeue();
370
- expect(probe).toBeDefined();
371
- const secondAttempt = await queue.dequeue();
372
- expect(secondAttempt).toBeUndefined(); // Only 1 task allowed to RUN in Half-Open
373
- });
374
- it('should not stall forever when circuit breaker is Half-Open and no tasks are found', async () => {
375
- // 1. Trip the breaker
376
- await queue.enqueue('foo', { foo: '1' });
377
- await queue.enqueue('foo', { foo: '2' });
378
- await queue.fail((await queue.dequeue()), 'err', { fatal: true });
379
- await queue.fail((await queue.dequeue()), 'err', { fatal: true });
380
- const cb = injector.resolve(CircuitBreaker, queue.namespace);
381
- let status = await cb.check();
382
- expect(status.state).toBe(CircuitBreakerState.Open);
383
- expect(status.allowed).toBe(false);
384
- // 2. Wait for timeout (50ms)
385
- await timeout(75);
386
- // 3. First dequeue - should be the probe, but find nothing (we enqueued and failed the only task)
387
- const tasks1 = await queue.dequeueMany(1);
388
- expect(tasks1).toHaveLength(0);
389
- status = await cb.check();
390
- expect(status.state).toBe(CircuitBreakerState.HalfOpen);
391
- // 4. Second dequeue - should still be allowed (but find nothing)
392
- const tasks2 = await queue.dequeueMany(1);
393
- expect(tasks2).toHaveLength(0);
394
- // 5. Add a task
395
- await queue.enqueue('foo', { foo: 'new' });
396
- // 6. Third dequeue - should find the task now!
397
- const tasks3 = await queue.dequeueMany(1);
398
- expect(tasks3).toHaveLength(1);
399
- });
400
- });
401
- describe('Timeouts and Maintenance (Pruning)', () => {
402
- it('should recover "Zombie" tasks (crashed workers) and preserve startTimestamp', async () => {
403
- const task = await queue.enqueue('foo', { foo: 'zombie' });
404
- const dequeued = await queue.dequeue(); // Task is now Running with a token
405
- expect(dequeued?.startTimestamp).not.toBeNull();
406
- const startTimestamp = dequeued.startTimestamp;
407
- // processTimeout is 50ms. Wait for it to expire.
408
- await timeout(100);
409
- await queue.maintenance();
410
- const recovered = await queue.getTask(task.id);
411
- expect(recovered?.status).toBe(TaskStatus.Retrying);
412
- expect(recovered?.tries).toBe(1);
413
- expect(recovered?.token).toBeNull();
414
- expect(recovered?.startTimestamp).toBe(startTimestamp);
415
- });
416
- it('should fail tasks that exceed Hard Execution Timeout via prune', async () => {
417
- // Re-configure queue with very short execution timeout
418
- const queueProvider = injector.resolve(TaskQueueProvider);
419
- const shortQueue = queueProvider.get(`prune-test-${crypto.randomUUID()}`, { maxExecutionTime: 50 });
420
- const task = await shortQueue.enqueue('foo', { foo: 'long-running' });
421
- await shortQueue.dequeue();
422
- await timeout(75);
423
- await shortQueue.maintenance();
424
- const updated = await shortQueue.getTask(task.id);
425
- expect(updated?.status).toBe(TaskStatus.TimedOut);
426
- expect(updated?.errors[0]?.code).toBe('MaxTimeExceeded');
427
- await shortQueue.clear();
428
- });
429
- it('should touch a task to extend token', async () => {
430
- const task = await queue.enqueue('foo', { foo: 'work' });
431
- const dequeued = await queue.dequeue();
432
- const initialLock = dequeued.visibilityDeadline;
433
- await timeout(20);
434
- const touched = await queue.touch(dequeued);
435
- expect(touched.visibilityDeadline > initialLock).toBe(true);
436
- });
437
- it('should prevent touching if token is lost (stolen by another worker)', async () => {
438
- await queue.enqueue('foo', { foo: 'work' });
439
- const dequeued = await queue.dequeue();
440
- expect(dequeued).toBeDefined();
441
- // processTimeout is 50ms. Wait for it to expire.
442
- await timeout(100);
443
- await queue.maintenance();
444
- await queue.dequeue(); // Stolen by another worker (tries=2)
445
- // Original worker tries to touch
446
- const touchResult = await queue.touch(dequeued);
447
- expect(touchResult).toBeUndefined();
448
- });
449
- it('should correctly identify and transition zombie tasks to Orphaned after max retries', async () => {
450
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, { maxTries: 1 });
451
- const task = await q.enqueue('test', { value: 'zombie' });
452
- // Dequeue to start it
453
- await q.dequeue();
454
- // Manually make it a zombie
455
- await database
456
- .update(taskTable)
457
- .set({ visibilityDeadline: currentTimestamp() - 1000 })
458
- .where(eq(taskTable.id, task.id));
459
- await q.maintenance();
460
- const updated = await q.getTask(task.id);
461
- expect(updated?.status).toBe(TaskStatus.Orphaned);
462
- await q.clear();
463
- });
464
- it('should age priority correctly', async () => {
465
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, {
466
- priorityAgingInterval: 0,
467
- priorityAgingStep: 10,
468
- });
469
- const task = await q.enqueue('test', { value: 'aging' }, { priority: 100 });
470
- // Manually backdate priorityAgeTimestamp
471
- await database
472
- .update(taskTable)
473
- .set({ priorityAgeTimestamp: currentTimestamp() - 1000 })
474
- .where(eq(taskTable.id, task.id));
475
- await q.maintenance();
476
- const updated = await q.getTask(task.id);
477
- expect(updated?.priority).toBe(90);
478
- await q.clear();
479
- });
480
- it('clear() should NOT throw foreign key violations', async () => {
481
- const queueProvider = injector.resolve(TaskQueueProvider);
482
- const otherQueue = queueProvider.get('other-namespace', {});
483
- const parent = await queue.enqueue('parent', {});
484
- await otherQueue.enqueue('child', {}, { parentId: parent.id });
485
- // Clearing the queue containing the parent should NOT throw even if other-namespace has a child pointing to it.
486
- await otherQueue.clear();
487
- await expect(queue.clear()).resolves.toBeUndefined();
488
- });
489
- });
490
- describe('Batch Operations', () => {
491
- it('should complete many tasks efficiently', async () => {
492
- const tasks = await queue.enqueueMany([
493
- { type: 'foo', data: { foo: '1' } },
494
- { type: 'foo', data: { foo: '2' } },
495
- ], { returnTasks: true });
496
- const d1 = await queue.dequeue();
497
- const d2 = await queue.dequeue();
498
- await queue.completeMany([d1, d2]);
499
- const t1 = await queue.getTask(tasks[0].id);
500
- const t2 = await queue.getTask(tasks[1].id);
501
- expect(t1?.status).toBe(TaskStatus.Completed);
502
- expect(t2?.status).toBe(TaskStatus.Completed);
503
- });
504
- it('should successfully completeMany tasks that have a NULL token', async () => {
505
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
506
- const t1 = await q.enqueue('test', { value: '1' });
507
- const t2 = await q.enqueue('test', { value: '2' });
508
- expect(t1.token).toBeNull();
509
- expect(t2.token).toBeNull();
510
- await q.completeMany([t1, t2]);
511
- const ut1 = await q.getTask(t1.id);
512
- const ut2 = await q.getTask(t2.id);
513
- expect(ut1?.status).toBe(TaskStatus.Completed);
514
- expect(ut2?.status).toBe(TaskStatus.Completed);
515
- await q.clear();
516
- });
517
- it('should successfully failMany tasks that have a NULL token', async () => {
518
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`, { maxTries: 0 });
519
- const t1 = await q.enqueue('test', { value: '1' });
520
- await q.failMany([t1], [new Error('fail')]);
521
- const ut1 = await q.getTask(t1.id);
522
- expect(ut1?.status).toBe(TaskStatus.Dead);
523
- await q.clear();
524
- });
525
- it('should successfully touchMany tasks with missing or NULL states', async () => {
526
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
527
- const t1 = await q.enqueue('test', { value: '1' });
528
- const t2 = await q.enqueue('test', { value: '2' });
529
- const d1 = await q.dequeue();
530
- const d2 = await q.dequeue();
531
- await q.touchMany([d1, d2], {
532
- progresses: [0.5, 0.8],
533
- states: [{ step: 'halfway' }, undefined],
534
- });
535
- const ut1 = await q.getTask(t1.id);
536
- const ut2 = await q.getTask(t2.id);
537
- expect(ut1?.progress).toBe(0.5);
538
- expect(ut1?.state).toEqual({ step: 'halfway' });
539
- expect(ut2?.progress).toBe(0.8);
540
- expect(ut2?.state).toBeNull();
541
- await q.clear();
542
- });
543
- it('should reject bulk updates (completeMany/failMany) if the provided token does not match the database', async () => {
544
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
545
- const t1 = await q.enqueue('test', { value: '1' });
546
- // Task is Pending, token is NULL.
547
- // Try to complete with a fake token.
548
- await q.completeMany([{ ...t1, token: crypto.randomUUID() }]);
549
- const ut1 = await q.getTask(t1.id);
550
- expect(ut1?.status).toBe(TaskStatus.Pending); // Should still be Pending
551
- // Dequeue to get a real token
552
- const d1 = await q.dequeue();
553
- expect(d1?.token).toBeDefined();
554
- // Try to complete with NULL token
555
- await q.completeMany([{ ...d1, token: null }]);
556
- const ut2 = await q.getTask(t1.id);
557
- expect(ut2?.status).toBe(TaskStatus.Running); // Should still be Running
558
- await q.clear();
559
- });
560
- });
561
- describe('Rescheduling', () => {
562
- it('should reschedule and refund tries if running', async () => {
563
- const task = await queue.enqueue('foo', { foo: 'reschedule-me' });
564
- const dequeued = await queue.dequeue();
565
- expect(dequeued?.tries).toBe(1);
566
- const inFuture = currentTimestamp() + 1000;
567
- await queue.reschedule(dequeued.id, inFuture);
568
- const updated = await queue.getTask(task.id);
569
- expect(updated?.status).toBe(TaskStatus.Pending);
570
- expect(updated?.tries).toBe(0); // Refunded
571
- expect(updated?.scheduleTimestamp).toBe(inFuture);
572
- });
573
- it('rescheduling should NOT bypass dependency constraints (Bug 7)', async () => {
574
- const dep = await queue.enqueue('dep', {});
575
- const main = await queue.enqueue('main', {}, { scheduleAfter: [dep.id] });
576
- expect(main.status).toBe(TaskStatus.Waiting);
577
- expect(main.unresolvedScheduleDependencies).toBe(1);
578
- await queue.reschedule(main.id, currentTimestamp());
579
- const updatedMain = await queue.getTask(main.id);
580
- expect(updatedMain?.status).toBe(TaskStatus.Waiting);
581
- });
582
- });
583
- describe('TaskContext (Worker DX)', () => {
584
- it('checkpoint() should update progress and handle token loss', async () => {
585
- const task = await queue.enqueue('foo', { foo: 'progress' });
586
- const dequeued = await queue.dequeue();
587
- // In real scenarios TaskContext wraps the queue logic.
588
- // Here we just verify touch/checkpoint effects on the DB.
589
- await queue.touch(dequeued, { progress: 0.5, state: { step: 1 } });
590
- const updated = await queue.getTask(task.id);
591
- expect(updated?.progress).toBe(0.5);
592
- expect(updated?.state).toEqual({ step: 1 });
593
- });
594
- });
595
- describe('waitForTasks', () => {
596
- it('should wait for multiple tasks to reach finalized state', async () => {
597
- const t1 = await queue.enqueue('foo', { foo: 'wait-1' });
598
- const t2 = await queue.enqueue('foo', { foo: 'wait-2' });
599
- void (async () => {
600
- await timeout(100);
601
- const d1 = await queue.dequeue();
602
- await queue.complete(d1);
603
- await timeout(100);
604
- const d2 = await queue.dequeue();
605
- await queue.complete(d2);
606
- })();
607
- const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
608
- expect(result.cancelled).toBe(false);
609
- const check1 = await queue.getTask(t1.id);
610
- const check2 = await queue.getTask(t2.id);
611
- expect(check1?.status).toBe(TaskStatus.Completed);
612
- expect(check2?.status).toBe(TaskStatus.Completed);
613
- });
614
- it('should throw TimeoutError on timeout', async () => {
615
- const t1 = await queue.enqueue('foo', { foo: 'timeout' });
616
- await expect(queue.waitForTasks([t1.id], { timeout: 100 })).rejects.toThrow('Timeout while waiting for tasks to complete');
617
- });
618
- it('should wait for Cancelled and Dead states', async () => {
619
- const t1 = await queue.enqueue('foo', { foo: 'cancel' });
620
- const t2 = await queue.enqueue('foo', { foo: 'dead' });
621
- void (async () => {
622
- await timeout(50);
623
- const d1 = await queue.dequeue();
624
- if (d1)
625
- await queue.cancel(d1.id);
626
- const d2 = await queue.dequeue();
627
- if (d2)
628
- await queue.fail(d2, new Error('fatal'), { fatal: true });
629
- queue.notify();
630
- })();
631
- const result = await queue.waitForTasks([t1.id, t2.id], { timeout: 2000 });
632
- expect(result.cancelled).toBe(false);
633
- const c1 = await queue.getTask(t1.id);
634
- const c2 = await queue.getTask(t2.id);
635
- expect(c1?.status).toBe(TaskStatus.Cancelled);
636
- expect(c2?.status).toBe(TaskStatus.Dead);
637
- });
638
- it('should handle cancellationSignal', async () => {
639
- const t1 = await queue.enqueue('foo', { foo: 'long' });
640
- const signal = new CancellationToken();
641
- void timeout(100).then(() => signal.set());
642
- const result = await queue.waitForTasks([t1.id], { cancellationSignal: signal, timeout: 5000 });
643
- expect(result.cancelled).toBe(true);
644
- });
645
- it('should return immediately for non-existent tasks', async () => {
646
- const result = await queue.waitForTasks([crypto.randomUUID()], { timeout: 1000 });
647
- expect(result.cancelled).toBe(false);
648
- });
649
- it('should return immediately if all tasks are already finalized', async () => {
650
- const t1 = await queue.enqueue('foo', { foo: 'immediate' });
651
- const d1 = await queue.dequeue();
652
- await queue.complete(d1);
653
- const timer = Timer.startNew();
654
- const result = await queue.waitForTasks([t1.id], { timeout: 1000 });
655
- expect(result.cancelled).toBe(false);
656
- expect(timer.milliseconds).toBeLessThan(100);
657
- });
658
- it('should handle a mix of active and archived tasks', async () => {
659
- const queueProvider = injector.resolve(TaskQueueProvider);
660
- const archiveQueue = queueProvider.get(`archive-test-${crypto.randomUUID()}`, {
661
- retention: 0, // Archive immediately
662
- });
663
- const t1 = await archiveQueue.enqueue('foo', { foo: 'archived' });
664
- const d1 = await archiveQueue.dequeue();
665
- await archiveQueue.complete(d1);
666
- // Run maintenance to move to archive
667
- await archiveQueue.maintenance();
668
- const t2 = await archiveQueue.enqueue('foo', { foo: 'active' });
669
- const d2 = await archiveQueue.dequeue();
670
- await archiveQueue.complete(d2);
671
- // t1 is archived, t2 is completed (active)
672
- const result = await archiveQueue.waitForTasks([t1.id, t2.id], { timeout: 1000 });
673
- expect(result.cancelled).toBe(false);
674
- await archiveQueue.clear();
675
- });
676
- it('should return immediately for empty ids array', async () => {
677
- const result = await queue.waitForTasks([], { timeout: 1000 });
678
- expect(result.cancelled).toBe(false);
679
- });
680
- it('should wait for parent task to reach finalized state after child completion', async () => {
681
- const parent = await queue.enqueue('parent', { value: 'parent' });
682
- const dParent = await queue.dequeue({ types: ['parent'] });
683
- expect(dParent).toBeDefined();
684
- // Spawn a child
685
- const [child] = await queue.enqueueMany([{ type: 'child', data: { value: 'child' }, parentId: parent.id, completeAfter: [] }], { returnTasks: true });
686
- expect(child).toBeDefined();
687
- // Re-enqueuing with dependency:
688
- const parentWithDep = await queue.enqueue('parent-dep', { value: 'parent' }, { completeAfter: [child.id] });
689
- let dParent2;
690
- for (let i = 0; i < 10; i++) {
691
- dParent2 = await queue.dequeue({ types: ['parent-dep'] });
692
- if (dParent2)
693
- break;
694
- await timeout(50);
695
- }
696
- expect(dParent2?.id).toBe(parentWithDep.id);
697
- // Complete parent (it will move to Waiting because of completeAfter)
698
- await queue.complete(dParent2);
699
- const checkParent = await queue.getTask(parentWithDep.id);
700
- expect(checkParent?.status).toBe(TaskStatus.WaitingChildren);
701
- void (async () => {
702
- await timeout(100);
703
- const dChild = await queue.dequeue({ types: ['child'] });
704
- await queue.complete(dChild);
705
- })();
706
- await queue.waitForTasks([parentWithDep.id], { timeout: 2000 });
707
- const finalParent = await queue.getTask(parentWithDep.id);
708
- expect(finalParent?.status).toBe(TaskStatus.Completed);
709
- });
710
- });
711
- describe('Bugs and Edge Cases', () => {
712
- it('should NOT overwrite terminal states during cancellation (Bug 3)', async () => {
713
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
714
- // 1. Enqueue parent and two children
715
- const parent = await q.enqueue('parent', {});
716
- const child1 = await q.enqueue('child', {}, { parentId: parent.id });
717
- const child2 = await q.enqueue('child', {}, { parentId: parent.id });
718
- // 2. Complete child1
719
- const dequeued1 = await q.dequeue({ types: ['child'] });
720
- await q.complete(dequeued1);
721
- const finished1 = await q.getTask(child1.id);
722
- expect(finished1?.status).toBe(TaskStatus.Completed);
723
- // 3. Cancel parent tree
724
- await q.cancel(parent.id);
725
- // 4. Verify child1 is STILL Completed, not Cancelled
726
- const updated1 = await q.getTask(child1.id);
727
- expect(updated1?.status).toBe(TaskStatus.Completed);
728
- // 5. Verify parent and child2 ARE Cancelled
729
- const updatedParent = await q.getTask(parent.id);
730
- const updated2 = await q.getTask(child2.id);
731
- expect(updatedParent?.status).toBe(TaskStatus.Cancelled);
732
- expect(updated2?.status).toBe(TaskStatus.Cancelled);
733
- await q.clear();
734
- });
735
- it('should expire tasks in Waiting status (Bug 5)', async () => {
736
- const q = injector.resolve(TaskQueueProvider).get(`q-${crypto.randomUUID()}`);
737
- // 1. Enqueue task A (will never complete)
738
- const taskA = await q.enqueue('test', {});
739
- // 2. Enqueue task B with scheduleAfter: [A.id] and short TTL
740
- const taskB = await q.enqueue('test', {}, {
741
- scheduleAfter: [taskA.id],
742
- timeToLive: 100, // 100ms
743
- });
744
- expect(taskB.status).toBe(TaskStatus.Waiting);
745
- // 3. Wait for TTL to pass
746
- await timeout(200);
747
- // 4. Run maintenance
748
- await q.maintenance();
749
- // 5. Verify task B is Expired
750
- await q.waitForTasks([taskB.id]);
751
- const updatedB = await q.getTask(taskB.id);
752
- expect(updatedB?.status).toBe(TaskStatus.Expired);
753
- await q.clear();
754
- });
755
- });
756
- });