atomic-queues 1.6.2 → 2.0.1

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 (337) hide show
  1. package/README.md +273 -413
  2. package/dist/cli/generators/json-schema.d.ts +3 -0
  3. package/dist/cli/generators/json-schema.d.ts.map +1 -0
  4. package/dist/cli/generators/json-schema.js +31 -0
  5. package/dist/cli/generators/json-schema.js.map +1 -0
  6. package/dist/cli/generators/typescript.d.ts +3 -0
  7. package/dist/cli/generators/typescript.d.ts.map +1 -0
  8. package/dist/cli/generators/typescript.js +62 -0
  9. package/dist/cli/generators/typescript.js.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +156 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/decorators/actor.decorators.d.ts +4 -0
  15. package/dist/decorators/actor.decorators.d.ts.map +1 -0
  16. package/dist/decorators/actor.decorators.js +32 -0
  17. package/dist/decorators/actor.decorators.js.map +1 -0
  18. package/dist/decorators/constants.d.ts +5 -12
  19. package/dist/decorators/constants.d.ts.map +1 -1
  20. package/dist/decorators/constants.js +10 -16
  21. package/dist/decorators/constants.js.map +1 -1
  22. package/dist/decorators/index.d.ts +4 -4
  23. package/dist/decorators/index.d.ts.map +1 -1
  24. package/dist/decorators/index.js +4 -4
  25. package/dist/decorators/index.js.map +1 -1
  26. package/dist/decorators/interfaces.d.ts +18 -78
  27. package/dist/decorators/interfaces.d.ts.map +1 -1
  28. package/dist/decorators/metadata-readers.d.ts +5 -26
  29. package/dist/decorators/metadata-readers.d.ts.map +1 -1
  30. package/dist/decorators/metadata-readers.js +16 -33
  31. package/dist/decorators/metadata-readers.js.map +1 -1
  32. package/dist/decorators/schema.decorators.d.ts +2 -0
  33. package/dist/decorators/schema.decorators.d.ts.map +1 -0
  34. package/dist/decorators/schema.decorators.js +13 -0
  35. package/dist/decorators/schema.decorators.js.map +1 -0
  36. package/dist/domain/interfaces/config.interfaces.d.ts +52 -153
  37. package/dist/domain/interfaces/config.interfaces.d.ts.map +1 -1
  38. package/dist/domain/interfaces/index.d.ts +0 -7
  39. package/dist/domain/interfaces/index.d.ts.map +1 -1
  40. package/dist/domain/interfaces/index.js +0 -7
  41. package/dist/domain/interfaces/index.js.map +1 -1
  42. package/dist/domain/interfaces/job.interfaces.d.ts +32 -65
  43. package/dist/domain/interfaces/job.interfaces.d.ts.map +1 -1
  44. package/dist/index.d.ts +0 -34
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +0 -39
  47. package/dist/index.js.map +1 -1
  48. package/dist/module/atomic-queues.module.d.ts +0 -83
  49. package/dist/module/atomic-queues.module.d.ts.map +1 -1
  50. package/dist/module/atomic-queues.module.js +35 -134
  51. package/dist/module/atomic-queues.module.js.map +1 -1
  52. package/dist/services/actor-registry/actor-registry.service.d.ts +30 -0
  53. package/dist/services/actor-registry/actor-registry.service.d.ts.map +1 -0
  54. package/dist/services/actor-registry/actor-registry.service.js +186 -0
  55. package/dist/services/actor-registry/actor-registry.service.js.map +1 -0
  56. package/dist/services/actor-registry/index.d.ts +2 -0
  57. package/dist/services/actor-registry/index.d.ts.map +1 -0
  58. package/dist/services/actor-registry/index.js +18 -0
  59. package/dist/services/actor-registry/index.js.map +1 -0
  60. package/dist/services/actor-system/actor-system.service.d.ts +19 -0
  61. package/dist/services/actor-system/actor-system.service.d.ts.map +1 -0
  62. package/dist/services/actor-system/actor-system.service.js +86 -0
  63. package/dist/services/actor-system/actor-system.service.js.map +1 -0
  64. package/dist/services/actor-system/index.d.ts +2 -0
  65. package/dist/services/actor-system/index.d.ts.map +1 -0
  66. package/dist/services/{cron-manager → actor-system}/index.js +1 -1
  67. package/dist/services/actor-system/index.js.map +1 -0
  68. package/dist/services/command-discovery/command-discovery.service.d.ts +6 -53
  69. package/dist/services/command-discovery/command-discovery.service.d.ts.map +1 -1
  70. package/dist/services/command-discovery/command-discovery.service.js +0 -59
  71. package/dist/services/command-discovery/command-discovery.service.js.map +1 -1
  72. package/dist/services/constants.d.ts +2 -9
  73. package/dist/services/constants.d.ts.map +1 -1
  74. package/dist/services/constants.js +3 -10
  75. package/dist/services/constants.js.map +1 -1
  76. package/dist/services/executor-pool/executor-pool.service.d.ts +31 -0
  77. package/dist/services/executor-pool/executor-pool.service.d.ts.map +1 -0
  78. package/dist/services/executor-pool/executor-pool.service.js +147 -0
  79. package/dist/services/executor-pool/executor-pool.service.js.map +1 -0
  80. package/dist/services/executor-pool/index.d.ts +2 -0
  81. package/dist/services/executor-pool/index.d.ts.map +1 -0
  82. package/dist/services/{queue-manager → executor-pool}/index.js +1 -1
  83. package/dist/services/executor-pool/index.js.map +1 -0
  84. package/dist/services/gate/gate.service.d.ts +17 -0
  85. package/dist/services/gate/gate.service.d.ts.map +1 -0
  86. package/dist/services/gate/gate.service.js +66 -0
  87. package/dist/services/gate/gate.service.js.map +1 -0
  88. package/dist/services/gate/index.d.ts +2 -0
  89. package/dist/services/gate/index.d.ts.map +1 -0
  90. package/dist/services/{spawn-queue → gate}/index.js +1 -1
  91. package/dist/services/gate/index.js.map +1 -0
  92. package/dist/services/handler-executor/handler-executor.service.d.ts +32 -0
  93. package/dist/services/handler-executor/handler-executor.service.d.ts.map +1 -0
  94. package/dist/services/handler-executor/handler-executor.service.js +186 -0
  95. package/dist/services/handler-executor/handler-executor.service.js.map +1 -0
  96. package/dist/services/handler-executor/index.d.ts +2 -0
  97. package/dist/services/handler-executor/index.d.ts.map +1 -0
  98. package/dist/services/handler-executor/index.js +18 -0
  99. package/dist/services/handler-executor/index.js.map +1 -0
  100. package/dist/services/index.d.ts +11 -12
  101. package/dist/services/index.d.ts.map +1 -1
  102. package/dist/services/index.js +11 -12
  103. package/dist/services/index.js.map +1 -1
  104. package/dist/services/log/index.d.ts +2 -0
  105. package/dist/services/log/index.d.ts.map +1 -0
  106. package/dist/services/{index-manager → log}/index.js +1 -1
  107. package/dist/services/log/index.js.map +1 -0
  108. package/dist/services/log/log.service.d.ts +21 -0
  109. package/dist/services/log/log.service.d.ts.map +1 -0
  110. package/dist/services/log/log.service.js +92 -0
  111. package/dist/services/log/log.service.js.map +1 -0
  112. package/dist/services/queue-bus/index.d.ts +0 -4
  113. package/dist/services/queue-bus/index.d.ts.map +1 -1
  114. package/dist/services/queue-bus/index.js +0 -4
  115. package/dist/services/queue-bus/index.js.map +1 -1
  116. package/dist/services/queue-bus/queue-bus.service.d.ts +44 -198
  117. package/dist/services/queue-bus/queue-bus.service.d.ts.map +1 -1
  118. package/dist/services/queue-bus/queue-bus.service.js +103 -259
  119. package/dist/services/queue-bus/queue-bus.service.js.map +1 -1
  120. package/dist/services/queue-bus/queue-bus.utils.d.ts +0 -28
  121. package/dist/services/queue-bus/queue-bus.utils.d.ts.map +1 -1
  122. package/dist/services/queue-bus/queue-bus.utils.js +1 -41
  123. package/dist/services/queue-bus/queue-bus.utils.js.map +1 -1
  124. package/dist/services/registry/index.d.ts +4 -0
  125. package/dist/services/registry/index.d.ts.map +1 -0
  126. package/dist/services/{queue-events-manager → registry}/index.js +3 -1
  127. package/dist/services/registry/index.js.map +1 -0
  128. package/dist/services/registry/registry.service.d.ts +43 -0
  129. package/dist/services/registry/registry.service.d.ts.map +1 -0
  130. package/dist/services/registry/registry.service.js +379 -0
  131. package/dist/services/registry/registry.service.js.map +1 -0
  132. package/dist/services/registry/registry.types.d.ts +24 -0
  133. package/dist/services/registry/registry.types.d.ts.map +1 -0
  134. package/dist/{domain/interfaces/lock.interfaces.js → services/registry/registry.types.js} +1 -1
  135. package/dist/services/registry/registry.types.js.map +1 -0
  136. package/dist/services/registry/schema-converter.d.ts +2 -0
  137. package/dist/services/registry/schema-converter.d.ts.map +1 -0
  138. package/dist/services/registry/schema-converter.js +27 -0
  139. package/dist/services/registry/schema-converter.js.map +1 -0
  140. package/dist/services/result-collector/index.d.ts +2 -0
  141. package/dist/services/result-collector/index.d.ts.map +1 -0
  142. package/dist/services/result-collector/index.js +18 -0
  143. package/dist/services/result-collector/index.js.map +1 -0
  144. package/dist/services/result-collector/result-collector.service.d.ts +17 -0
  145. package/dist/services/result-collector/result-collector.service.d.ts.map +1 -0
  146. package/dist/services/result-collector/result-collector.service.js +92 -0
  147. package/dist/services/result-collector/result-collector.service.js.map +1 -0
  148. package/dist/services/scheduler/index.d.ts +2 -0
  149. package/dist/services/scheduler/index.d.ts.map +1 -0
  150. package/dist/services/{job-processor → scheduler}/index.js +1 -1
  151. package/dist/services/scheduler/index.js.map +1 -0
  152. package/dist/services/scheduler/scheduler.service.d.ts +17 -0
  153. package/dist/services/scheduler/scheduler.service.d.ts.map +1 -0
  154. package/dist/services/scheduler/scheduler.service.js +116 -0
  155. package/dist/services/scheduler/scheduler.service.js.map +1 -0
  156. package/dist/services/shutdown/index.d.ts +2 -0
  157. package/dist/services/shutdown/index.d.ts.map +1 -0
  158. package/dist/services/shutdown/index.js +18 -0
  159. package/dist/services/shutdown/index.js.map +1 -0
  160. package/dist/services/shutdown/shutdown.service.d.ts +8 -0
  161. package/dist/services/shutdown/shutdown.service.d.ts.map +1 -0
  162. package/dist/services/shutdown/shutdown.service.js +29 -0
  163. package/dist/services/shutdown/shutdown.service.js.map +1 -0
  164. package/dist/utils/index.d.ts +3 -1
  165. package/dist/utils/index.d.ts.map +1 -1
  166. package/dist/utils/index.js +3 -1
  167. package/dist/utils/index.js.map +1 -1
  168. package/dist/utils/naming.utils.d.ts +0 -16
  169. package/dist/utils/naming.utils.d.ts.map +1 -1
  170. package/dist/utils/naming.utils.js +0 -29
  171. package/dist/utils/naming.utils.js.map +1 -1
  172. package/package.json +19 -11
  173. package/dist/decorators/legacy.decorators.d.ts +0 -36
  174. package/dist/decorators/legacy.decorators.d.ts.map +0 -1
  175. package/dist/decorators/legacy.decorators.js +0 -61
  176. package/dist/decorators/legacy.decorators.js.map +0 -1
  177. package/dist/decorators/scaler.decorators.d.ts +0 -65
  178. package/dist/decorators/scaler.decorators.d.ts.map +0 -1
  179. package/dist/decorators/scaler.decorators.js +0 -103
  180. package/dist/decorators/scaler.decorators.js.map +0 -1
  181. package/dist/decorators/type-guards.d.ts +0 -18
  182. package/dist/decorators/type-guards.d.ts.map +0 -1
  183. package/dist/decorators/type-guards.js +0 -32
  184. package/dist/decorators/type-guards.js.map +0 -1
  185. package/dist/decorators/worker.decorators.d.ts +0 -58
  186. package/dist/decorators/worker.decorators.d.ts.map +0 -1
  187. package/dist/decorators/worker.decorators.js +0 -92
  188. package/dist/decorators/worker.decorators.js.map +0 -1
  189. package/dist/domain/interfaces/event.interfaces.d.ts +0 -71
  190. package/dist/domain/interfaces/event.interfaces.d.ts.map +0 -1
  191. package/dist/domain/interfaces/event.interfaces.js +0 -3
  192. package/dist/domain/interfaces/event.interfaces.js.map +0 -1
  193. package/dist/domain/interfaces/index-tracking.interfaces.d.ts +0 -69
  194. package/dist/domain/interfaces/index-tracking.interfaces.d.ts.map +0 -1
  195. package/dist/domain/interfaces/index-tracking.interfaces.js +0 -3
  196. package/dist/domain/interfaces/index-tracking.interfaces.js.map +0 -1
  197. package/dist/domain/interfaces/lock.interfaces.d.ts +0 -54
  198. package/dist/domain/interfaces/lock.interfaces.d.ts.map +0 -1
  199. package/dist/domain/interfaces/lock.interfaces.js.map +0 -1
  200. package/dist/domain/interfaces/process.interfaces.d.ts +0 -44
  201. package/dist/domain/interfaces/process.interfaces.d.ts.map +0 -1
  202. package/dist/domain/interfaces/process.interfaces.js +0 -3
  203. package/dist/domain/interfaces/process.interfaces.js.map +0 -1
  204. package/dist/domain/interfaces/queue.interfaces.d.ts +0 -46
  205. package/dist/domain/interfaces/queue.interfaces.d.ts.map +0 -1
  206. package/dist/domain/interfaces/queue.interfaces.js +0 -3
  207. package/dist/domain/interfaces/queue.interfaces.js.map +0 -1
  208. package/dist/domain/interfaces/scaling.interfaces.d.ts +0 -62
  209. package/dist/domain/interfaces/scaling.interfaces.d.ts.map +0 -1
  210. package/dist/domain/interfaces/scaling.interfaces.js +0 -3
  211. package/dist/domain/interfaces/scaling.interfaces.js.map +0 -1
  212. package/dist/domain/interfaces/worker.interfaces.d.ts +0 -120
  213. package/dist/domain/interfaces/worker.interfaces.d.ts.map +0 -1
  214. package/dist/domain/interfaces/worker.interfaces.js +0 -3
  215. package/dist/domain/interfaces/worker.interfaces.js.map +0 -1
  216. package/dist/services/cron-manager/cron-manager.service.d.ts +0 -199
  217. package/dist/services/cron-manager/cron-manager.service.d.ts.map +0 -1
  218. package/dist/services/cron-manager/cron-manager.service.js +0 -583
  219. package/dist/services/cron-manager/cron-manager.service.js.map +0 -1
  220. package/dist/services/cron-manager/index.d.ts +0 -2
  221. package/dist/services/cron-manager/index.d.ts.map +0 -1
  222. package/dist/services/cron-manager/index.js.map +0 -1
  223. package/dist/services/index-manager/index-manager.service.d.ts +0 -142
  224. package/dist/services/index-manager/index-manager.service.d.ts.map +0 -1
  225. package/dist/services/index-manager/index-manager.service.js +0 -325
  226. package/dist/services/index-manager/index-manager.service.js.map +0 -1
  227. package/dist/services/index-manager/index.d.ts +0 -2
  228. package/dist/services/index-manager/index.d.ts.map +0 -1
  229. package/dist/services/index-manager/index.js.map +0 -1
  230. package/dist/services/job-processor/index.d.ts +0 -2
  231. package/dist/services/job-processor/index.d.ts.map +0 -1
  232. package/dist/services/job-processor/index.js.map +0 -1
  233. package/dist/services/job-processor/job-processor.service.d.ts +0 -156
  234. package/dist/services/job-processor/job-processor.service.d.ts.map +0 -1
  235. package/dist/services/job-processor/job-processor.service.js +0 -331
  236. package/dist/services/job-processor/job-processor.service.js.map +0 -1
  237. package/dist/services/processor-discovery/decorator-discovery.service.d.ts +0 -40
  238. package/dist/services/processor-discovery/decorator-discovery.service.d.ts.map +0 -1
  239. package/dist/services/processor-discovery/decorator-discovery.service.js +0 -191
  240. package/dist/services/processor-discovery/decorator-discovery.service.js.map +0 -1
  241. package/dist/services/processor-discovery/index.d.ts +0 -6
  242. package/dist/services/processor-discovery/index.d.ts.map +0 -1
  243. package/dist/services/processor-discovery/index.js +0 -22
  244. package/dist/services/processor-discovery/index.js.map +0 -1
  245. package/dist/services/processor-discovery/processor-discovery.service.d.ts +0 -98
  246. package/dist/services/processor-discovery/processor-discovery.service.d.ts.map +0 -1
  247. package/dist/services/processor-discovery/processor-discovery.service.js +0 -258
  248. package/dist/services/processor-discovery/processor-discovery.service.js.map +0 -1
  249. package/dist/services/processor-discovery/processor-registry.d.ts +0 -58
  250. package/dist/services/processor-discovery/processor-registry.d.ts.map +0 -1
  251. package/dist/services/processor-discovery/processor-registry.js +0 -74
  252. package/dist/services/processor-discovery/processor-registry.js.map +0 -1
  253. package/dist/services/processor-discovery/scaling-registration.service.d.ts +0 -60
  254. package/dist/services/processor-discovery/scaling-registration.service.d.ts.map +0 -1
  255. package/dist/services/processor-discovery/scaling-registration.service.js +0 -261
  256. package/dist/services/processor-discovery/scaling-registration.service.js.map +0 -1
  257. package/dist/services/processor-discovery/worker-factory.service.d.ts +0 -54
  258. package/dist/services/processor-discovery/worker-factory.service.d.ts.map +0 -1
  259. package/dist/services/processor-discovery/worker-factory.service.js +0 -185
  260. package/dist/services/processor-discovery/worker-factory.service.js.map +0 -1
  261. package/dist/services/queue-bus/entity-target.d.ts +0 -58
  262. package/dist/services/queue-bus/entity-target.d.ts.map +0 -1
  263. package/dist/services/queue-bus/entity-target.js +0 -109
  264. package/dist/services/queue-bus/entity-target.js.map +0 -1
  265. package/dist/services/queue-bus/queue-bus.types.d.ts +0 -40
  266. package/dist/services/queue-bus/queue-bus.types.d.ts.map +0 -1
  267. package/dist/services/queue-bus/queue-bus.types.js +0 -3
  268. package/dist/services/queue-bus/queue-bus.types.js.map +0 -1
  269. package/dist/services/queue-bus/queue-target.d.ts +0 -61
  270. package/dist/services/queue-bus/queue-target.d.ts.map +0 -1
  271. package/dist/services/queue-bus/queue-target.js +0 -123
  272. package/dist/services/queue-bus/queue-target.js.map +0 -1
  273. package/dist/services/queue-events-manager/index.d.ts +0 -2
  274. package/dist/services/queue-events-manager/index.d.ts.map +0 -1
  275. package/dist/services/queue-events-manager/index.js.map +0 -1
  276. package/dist/services/queue-events-manager/queue-events-manager.service.d.ts +0 -120
  277. package/dist/services/queue-events-manager/queue-events-manager.service.d.ts.map +0 -1
  278. package/dist/services/queue-events-manager/queue-events-manager.service.js +0 -343
  279. package/dist/services/queue-events-manager/queue-events-manager.service.js.map +0 -1
  280. package/dist/services/queue-manager/index.d.ts +0 -2
  281. package/dist/services/queue-manager/index.d.ts.map +0 -1
  282. package/dist/services/queue-manager/index.js.map +0 -1
  283. package/dist/services/queue-manager/queue-manager.service.d.ts +0 -148
  284. package/dist/services/queue-manager/queue-manager.service.d.ts.map +0 -1
  285. package/dist/services/queue-manager/queue-manager.service.js +0 -348
  286. package/dist/services/queue-manager/queue-manager.service.js.map +0 -1
  287. package/dist/services/resource-lock/index.d.ts +0 -2
  288. package/dist/services/resource-lock/index.d.ts.map +0 -1
  289. package/dist/services/resource-lock/index.js +0 -18
  290. package/dist/services/resource-lock/index.js.map +0 -1
  291. package/dist/services/resource-lock/resource-lock.service.d.ts +0 -120
  292. package/dist/services/resource-lock/resource-lock.service.d.ts.map +0 -1
  293. package/dist/services/resource-lock/resource-lock.service.js +0 -367
  294. package/dist/services/resource-lock/resource-lock.service.js.map +0 -1
  295. package/dist/services/service-queue/index.d.ts +0 -3
  296. package/dist/services/service-queue/index.d.ts.map +0 -1
  297. package/dist/services/service-queue/index.js +0 -19
  298. package/dist/services/service-queue/index.js.map +0 -1
  299. package/dist/services/service-queue/service-queue.service.d.ts +0 -199
  300. package/dist/services/service-queue/service-queue.service.d.ts.map +0 -1
  301. package/dist/services/service-queue/service-queue.service.js +0 -617
  302. package/dist/services/service-queue/service-queue.service.js.map +0 -1
  303. package/dist/services/service-queue/service-queue.types.d.ts +0 -32
  304. package/dist/services/service-queue/service-queue.types.d.ts.map +0 -1
  305. package/dist/services/service-queue/service-queue.types.js +0 -27
  306. package/dist/services/service-queue/service-queue.types.js.map +0 -1
  307. package/dist/services/shutdown-state/index.d.ts +0 -2
  308. package/dist/services/shutdown-state/index.d.ts.map +0 -1
  309. package/dist/services/shutdown-state/index.js +0 -18
  310. package/dist/services/shutdown-state/index.js.map +0 -1
  311. package/dist/services/shutdown-state/shutdown-state.service.d.ts +0 -69
  312. package/dist/services/shutdown-state/shutdown-state.service.d.ts.map +0 -1
  313. package/dist/services/shutdown-state/shutdown-state.service.js +0 -127
  314. package/dist/services/shutdown-state/shutdown-state.service.js.map +0 -1
  315. package/dist/services/spawn-queue/index.d.ts +0 -2
  316. package/dist/services/spawn-queue/index.d.ts.map +0 -1
  317. package/dist/services/spawn-queue/index.js.map +0 -1
  318. package/dist/services/spawn-queue/spawn-queue.service.d.ts +0 -119
  319. package/dist/services/spawn-queue/spawn-queue.service.d.ts.map +0 -1
  320. package/dist/services/spawn-queue/spawn-queue.service.js +0 -273
  321. package/dist/services/spawn-queue/spawn-queue.service.js.map +0 -1
  322. package/dist/services/worker-manager/index.d.ts +0 -2
  323. package/dist/services/worker-manager/index.d.ts.map +0 -1
  324. package/dist/services/worker-manager/index.js +0 -18
  325. package/dist/services/worker-manager/index.js.map +0 -1
  326. package/dist/services/worker-manager/worker-manager.service.d.ts +0 -221
  327. package/dist/services/worker-manager/worker-manager.service.d.ts.map +0 -1
  328. package/dist/services/worker-manager/worker-manager.service.js +0 -591
  329. package/dist/services/worker-manager/worker-manager.service.js.map +0 -1
  330. package/dist/utils/helpers.d.ts +0 -5
  331. package/dist/utils/helpers.d.ts.map +0 -1
  332. package/dist/utils/helpers.js +0 -21
  333. package/dist/utils/helpers.js.map +0 -1
  334. package/dist/utils/job.utils.d.ts +0 -50
  335. package/dist/utils/job.utils.d.ts.map +0 -1
  336. package/dist/utils/job.utils.js +0 -89
  337. package/dist/utils/job.utils.js.map +0 -1
package/README.md CHANGED
@@ -26,54 +26,29 @@
26
26
  <p align="center">
27
27
  <img src="https://img.shields.io/npm/v/atomic-queues?style=flat-square&color=cb3837" alt="npm version" />
28
28
  <img src="https://img.shields.io/badge/NestJS-11-ea2845?style=flat-square&logo=nestjs" alt="NestJS 11" />
29
- <img src="https://img.shields.io/badge/BullMQ-5-3c873a?style=flat-square" alt="BullMQ 5" />
30
29
  <img src="https://img.shields.io/badge/Redis-7-dc382d?style=flat-square&logo=redis&logoColor=white" alt="Redis 7" />
31
30
  <img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License" />
32
31
  </p>
33
32
 
34
33
  ---
35
34
 
36
- ## Why atomic-queues?
35
+ ## What is atomic-queues?
37
36
 
38
- Distributed locks (Redlock, advisory locks, optimistic locking) all share the same fundamental flaw: **contention collapse**. When multiple pods fight for the same lock simultaneously, they spend more time retrying failed acquisitions than doing actual work. The harder you push, the slower they go.
37
+ **A distributed virtual actor runtime for Node.js, built entirely on Redis primitives.**
39
38
 
40
- **atomic-queues** eliminates contention entirely. Instead of locking, each entity gets its own dedicated BullMQ queue. Operations execute sequentially back-to-back with zero wasted cycles. There's nothing to contend over.
39
+ Think [Microsoft Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/) or [Akka](https://akka.io/) but for the NestJS ecosystem, requiring nothing beyond a Redis instance you probably already have.
41
40
 
42
- ### atomic-queues vs Redlock
41
+ Messages addressed to the same entity execute sequentially. Messages addressed to different entities execute in parallel. No distributed locks. No worker processes. No message broker. No BullMQ.
43
42
 
44
- | | Redlock | atomic-queues |
45
- |---|---|---|
46
- | **Architecture** | Distributed mutex (quorum-based) | Per-entity queue (sequential) |
47
- | **Under contention** | Degrades — retry storms, backoff delays | **Constant** — jobs queue up, execute instantly |
48
- | **Failure mode** | Silent double-execution (clock drift) | Job stuck in queue (visible, retryable) |
49
- | **Split-brain risk** | Yes (timing assumptions) | **Impossible** (serial queue) |
50
- | **Warm-path overhead** | Acquire + release per op | **0 Redis calls** (in-memory hot cache) |
51
- | **Cold-start** | None | One-time per entity |
52
- | **Multi-pod scaling** | Contention increases with pods | **Throughput increases with pods** |
43
+ ```
44
+ npm install atomic-queues ioredis
45
+ ```
53
46
 
54
47
  ---
55
48
 
56
- ## Table of Contents
57
-
58
- - [Why atomic-queues?](#why-atomic-queues)
59
- - [How It Works](#how-it-works)
60
- - [Installation](#installation)
61
- - [Quick Start](#quick-start)
62
- - [Commands & Decorators](#commands--decorators)
63
- - [Configuration](#configuration)
64
- - [Distributed Worker Lifecycle](#distributed-worker-lifecycle)
65
- - [Complete Example](#complete-example)
66
- - [Advanced: Custom Worker Processors](#advanced-custom-worker-processors)
67
- - [Performance](#performance)
68
- - [License](#license)
49
+ ## The Problem
69
50
 
70
- ---
71
-
72
- ## How It Works
73
-
74
- ### The Problem
75
-
76
- Every distributed system eventually hits this:
51
+ Every distributed system eventually builds toward one of two failure modes: **state corruption** from concurrent mutations on the same entity, or **throughput collapse** from the locking mechanisms used to prevent it.
77
52
 
78
53
  ```
79
54
  Time Request A Request B Database
@@ -86,110 +61,80 @@ T₃ UPDATE: $100 − $80 = $20 −$60
86
61
  Result: Balance is −$60. Both withdrawals succeed. Integrity violated.
87
62
  ```
88
63
 
89
- ### The Solution
64
+ The standard answers — `SELECT ... FOR UPDATE`, optimistic locking with retries, distributed locks via Redlock or ZooKeeper, serializable transactions — all trade throughput for correctness. Under load, they become bottlenecks. Across services, they become nightmares. And every team ends up inventing some ad-hoc combination of them, poorly, under production pressure.
65
+
66
+ ## The Insight
90
67
 
91
- atomic-queues routes operations through per-entity queues. Same entity same queue sequential execution. Different entities parallel queues full throughput.
68
+ The problem disappears if you change *when* serialization happens. Instead of serializing at the database level (row locks, transaction isolation), serialize at the **message level**: route all operations for a given entity through a single ordered log, and process that log sequentially. Different entities maintain independent logs with zero coordination between them.
69
+
70
+ This is the virtual actor model. It's not new — Erlang/OTP has used it since the 1980s, Orleans shipped it in 2014, Akka has been doing it on the JVM for over a decade. What *is* new is implementing it with nothing beyond Redis and making it native to the NestJS ecosystem.
92
71
 
93
72
  ```
94
73
  ┌─────────────────────────────────────────────────┐
95
74
  Request A ─┐ │ Entity: account-42 │
96
75
  │ │ ┌──────┐ ┌──────┐ ┌──────┐ │
97
- Request B ─┼─► Route ─┼─►│ Op 1 │─►│ Op 2 │─►│ Op 3 │─► [Worker] ──┐
98
- │ │ └──────┘ └──────┘ └──────┘ │
99
- Request C ─┘ │ Sequential ◄─────────────┘
76
+ Request B ─┼─► Route ─┼─►│ Msg1 │─►│ Msg2 │─►│ Msg3 │─► [Executor] ─┐
77
+ │ │ └──────┘ └──────┘ └──────┘ │
78
+ Request C ─┘ │ Sequential ◄────────────┘
100
79
  └─────────────────────────────────────────────────┘
101
80
 
102
- ┌─────────────────────────────────────────────────┐
103
- Request D ─┐ │ Entity: account-99 │
104
- │ │ ┌──────┐ ┌──────┐ │
105
- Request E ─┼─► Route ─┼─►│ Op 1 │─►│ Op 2 │─────────► [Worker] ──┐ │
106
- │ │ └──────┘ └──────┘ │ │
107
- Request F ─┘ │ Sequential ◄───────────┘ │
108
- └─────────────────────────────────────────────────┘
109
-
110
- ▲ These two queues run in PARALLEL across pods ▲
81
+ Meanwhile, account-99, order-7, user-abc — all execute
82
+ in parallel on the same cluster, completely independent.
111
83
  ```
112
84
 
113
- **Key properties:**
114
- - **One worker per entity** — enforced via Redis heartbeat TTL. No duplicates, ever.
115
- - **Auto-spawn** — workers materialize when jobs arrive, on the pod that sees them first.
116
- - **Auto-terminate** — idle workers shut down after a configurable timeout.
117
- - **Self-healing** — node failure → heartbeat expires → worker respawns on a healthy pod.
118
- - **Distributed** — workers spread across all pods via atomic `SET NX` claim. No leader election, no single point of failure.
85
+ This eliminates an entire class of bugs — lost updates, dirty reads, write skew, phantom reads on hot entities — without pessimistic locks, without optimistic retries, and without the `SELECT ... FOR UPDATE` that your DBA tells you not to use under load. The entity itself becomes the consistency boundary, and the consistency is structural rather than transactional.
119
86
 
120
87
  ---
121
88
 
122
- ## Installation
123
-
124
- ```bash
125
- npm install atomic-queues
126
- ```
89
+ ## How It Works
127
90
 
128
- BullMQ, ioredis, and `@nestjs/bullmq` are bundled — no extra installs needed.
91
+ ### Entities and messages
129
92
 
130
- **Peer dependencies** (provided by your NestJS app): `@nestjs/common` 10+, `@nestjs/core` 10+, `reflect-metadata`, `rxjs` 7+. Optional: `@nestjs/cqrs` (for auto-routing commands/queries).
93
+ Everything in atomic-queues is an **entity** that receives **messages**. An entity is identified by a type and an ID `account:a-42`, `order:o-17`, `user:u-abc`. A message is a command or query addressed to a specific entity instance. You define this relationship with two decorators:
131
94
 
132
- ---
95
+ ```typescript
96
+ @EntityType('account')
97
+ export class WithdrawCommand {
98
+ constructor(
99
+ @QueueEntityId() public readonly accountId: string,
100
+ public readonly amount: number,
101
+ ) {}
102
+ }
103
+ ```
133
104
 
134
- ## Quick Start
105
+ That's the entire contract. `@EntityType` says "this message targets the `account` entity type." `@QueueEntityId()` says "the value of `accountId` is the entity instance ID." When you enqueue this command, the runtime routes it to the log for `account:{accountId}` and guarantees sequential execution against that specific entity instance, cluster-wide.
135
106
 
136
- ### 1. Configure the Module
107
+ ### Two levels of abstraction
137
108
 
138
- ```typescript
139
- import { Module } from '@nestjs/common';
140
- import { CqrsModule } from '@nestjs/cqrs';
141
- import { AtomicQueuesModule } from 'atomic-queues';
109
+ atomic-queues gives you two ways to handle messages, and they're not different systems — they're two levels of abstraction over the same dispatch engine.
142
110
 
143
- @Module({
144
- imports: [
145
- CqrsModule,
146
- AtomicQueuesModule.forRoot({
147
- redis: { host: 'localhost', port: 6379 },
148
- keyPrefix: 'myapp',
149
- entities: {
150
- account: {
151
- queueName: (id) => `account-${id}-queue`,
152
- workerName: (id) => `account-${id}-worker`,
153
- maxWorkersPerEntity: 1,
154
- idleTimeoutSeconds: 15,
155
- },
156
- },
157
- }),
158
- ],
159
- })
160
- export class AppModule {}
161
- ```
111
+ **Actors** are the foundational primitive. An actor class *is* an entity — its fields are the state, its methods are message handlers. The runtime manages its lifecycle: activate on first message, evict from memory on idle, persist state to Redis automatically, restore on reactivation.
162
112
 
163
- > **Tip:** The `entities` config is optional. Without it, default naming applies: `{keyPrefix}:{entityType}:{entityId}:queue`.
113
+ ```typescript
114
+ @Actor('account')
115
+ @Injectable()
116
+ export class AccountActor {
117
+ private balance = 0;
164
118
 
165
- <details>
166
- <summary><strong>Async configuration (ConfigService)</strong></summary>
119
+ @On(DepositCommand)
120
+ async deposit(msg: DepositCommand) {
121
+ this.balance += msg.amount;
122
+ return this.balance;
123
+ }
167
124
 
168
- ```typescript
169
- AtomicQueuesModule.forRootAsync({
170
- imports: [ConfigModule],
171
- useFactory: (config: ConfigService) => ({
172
- redis: { url: config.get('REDIS_URL') },
173
- keyPrefix: 'myapp',
174
- entities: {
175
- account: {
176
- queueName: (id) => `account-${id}-queue`,
177
- workerName: (id) => `account-${id}-worker`,
178
- },
179
- },
180
- }),
181
- inject: [ConfigService],
182
- }),
125
+ @On(WithdrawCommand)
126
+ async withdraw(msg: WithdrawCommand) {
127
+ if (this.balance < msg.amount) throw new InsufficientFunds();
128
+ this.balance -= msg.amount;
129
+ return this.balance;
130
+ }
131
+ }
183
132
  ```
184
133
 
185
- </details>
186
-
187
- ### 2. Define Commands
134
+ **CQRS handlers** are the convenience layer for teams using `@nestjs/cqrs`. You don't write actor classes — you write standard `@CommandHandler` and `@QueryHandler` classes exactly as NestJS CQRS prescribes, and atomic-queues intercepts the dispatch to route them through the same per-entity log and gate system. The handler code doesn't change. The guarantee changes — instead of executing inline on whatever request thread happens to call `commandBus.execute()`, your handler now executes sequentially per entity, cluster-wide.
188
135
 
189
136
  ```typescript
190
- import { QueueEntity, QueueEntityId } from 'atomic-queues';
191
-
192
- @QueueEntity('account')
137
+ @EntityType('account')
193
138
  export class WithdrawCommand {
194
139
  constructor(
195
140
  @QueueEntityId() public readonly accountId: string,
@@ -197,409 +142,324 @@ export class WithdrawCommand {
197
142
  ) {}
198
143
  }
199
144
 
200
- @QueueEntity('account')
201
- export class DepositCommand {
202
- constructor(
203
- @QueueEntityId() public readonly accountId: string,
204
- public readonly amount: number,
205
- ) {}
145
+ @CommandHandler(WithdrawCommand)
146
+ export class WithdrawHandler implements ICommandHandler<WithdrawCommand> {
147
+ async execute(cmd: WithdrawCommand) {
148
+ // This runs sequentially per account — cluster-wide.
149
+ // No locks. No transactions. The dispatch engine guarantees it.
150
+ }
206
151
  }
207
152
  ```
208
153
 
209
- ### 3. Write Handlers (standard @nestjs/cqrs)
154
+ The library auto-discovers `@CommandHandler` and `@QueryHandler` classes at boot and wires them into the dispatch pipeline. Your existing CQRS architecture gets per-entity sequential guarantees without changing a single handler. The CQRS surface *calls into the actor runtime* — it's not a separate execution path.
210
155
 
211
- ```typescript
212
- import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
156
+ ### Enqueuing messages
213
157
 
214
- @CommandHandler(WithdrawCommand)
215
- export class WithdrawHandler implements ICommandHandler<WithdrawCommand> {
216
- constructor(private readonly repo: AccountRepository) {}
158
+ ```typescript
159
+ // Fire-and-forget
160
+ await queueBus.enqueue(new WithdrawCommand(accountId, 100));
217
161
 
218
- async execute({ accountId, amount }: WithdrawCommand) {
219
- // SAFE: No race conditions. Sequential execution per account.
220
- const account = await this.repo.findById(accountId);
162
+ // Enqueue and block until result
163
+ const balance = await queueBus.enqueueAndWait(new GetBalanceQuery(accountId));
221
164
 
222
- if (account.balance < amount) {
223
- throw new InsufficientFundsError(accountId, account.balance, amount);
224
- }
165
+ // Scoped to an entity type
166
+ await queueBus.forEntity('account').enqueueBulk([charge1, charge2, charge3]);
225
167
 
226
- account.balance -= amount;
227
- await this.repo.save(account);
228
- }
229
- }
168
+ // Actor-style direct send
169
+ await actorSystem.send('account', accountId, new DepositCommand(100));
170
+ const balance = await actorSystem.sendAndWait('account', accountId, new GetBalanceQuery());
230
171
  ```
231
172
 
232
- ### 4. Enqueue Jobs
173
+ ---
233
174
 
234
- ```typescript
235
- import { Injectable } from '@nestjs/common';
236
- import { QueueBus } from 'atomic-queues';
175
+ ## The Dispatch Engine
237
176
 
238
- @Injectable()
239
- export class AccountService {
240
- constructor(private readonly queueBus: QueueBus) {}
177
+ Under every API call is the same pipeline: **message → Redis log → Lua scheduler → gate → executor → handler**. Understanding this pipeline is key to understanding what atomic-queues actually guarantees and why it can guarantee it without locks.
241
178
 
242
- async withdraw(accountId: string, amount: number) {
243
- await this.queueBus.enqueue(new WithdrawCommand(accountId, amount));
244
- }
245
- }
246
- ```
179
+ ### Per-entity message logs
247
180
 
248
- **That's it.** The library automatically:
249
- 1. Creates a queue for each `accountId` when jobs arrive
250
- 2. Spawns a worker (spread across pods) to process jobs sequentially
251
- 3. Routes jobs to the correct `@CommandHandler` via CQRS
252
- 4. Terminates idle workers after the configured timeout
253
- 5. Self-heals if a pod dies (heartbeat expires → respawn elsewhere)
181
+ When you call `enqueue()`, the message is serialized to JSON and appended to a Redis list (`LPUSH aq:log:account:a-42`), and the entity key is added to a global ready set (`SADD aq:ready account:a-42`). A pub/sub notification wakes the executor pool. Three Redis commands, pipelined in one round-trip.
254
182
 
255
- ---
183
+ The log is the source of truth for ordering. Redis lists are FIFO — `LPUSH` appends to the head, `RPOP` consumes from the tail. Messages for the same entity are always processed in enqueue order.
256
184
 
257
- ## Commands & Decorators
185
+ ### The dispatch gate
258
186
 
259
- ### `@QueueEntity(entityType, entityIdProperty?)`
187
+ The core consistency primitive is the **dispatch gate** — a Redis key per entity (`SET aq:gate:account:a-42 <token> EX 30 NX`). The `NX` flag means only one executor can acquire it. The `EX` TTL means a crashed executor releases it automatically. This is not a distributed lock in the Redlock sense — there's no quorum, no retry loop, no backoff. If the gate is held, the scheduler moves on to the next ready entity. Zero contention between entities, zero blocking within the scheduling loop.
260
188
 
261
- Marks a command/query class for queue routing. The optional second argument specifies which property holds the entity ID — this is the simplest approach when you don't want to decorate individual properties.
189
+ ### Atomic Lua scheduling
262
190
 
263
- ```typescript
264
- // Option 1: Explicit property name (no @QueueEntityId needed)
265
- @QueueEntity('account', 'accountId')
266
- export class TransferCommand {
267
- constructor(
268
- public readonly accountId: string,
269
- public readonly toAccountId: string,
270
- public readonly amount: number,
271
- ) {}
272
- }
191
+ A single Lua script runs atomically in Redis to perform the entire dispatch cycle:
273
192
 
274
- // Option 2: Rely on module-level defaultEntityId from entities config
275
- @QueueEntity('account')
276
- export class DepositCommand {
277
- constructor(
278
- public readonly accountId: string, // Matched by entities.account.defaultEntityId
279
- public readonly amount: number,
280
- ) {}
281
- }
282
- ```
193
+ 1. Sample entities from the ready set (`SRANDMEMBER` with batch size 32)
194
+ 2. Try to acquire the gate for each candidate (`SET NX EX`)
195
+ 3. On first successful acquisition, pop the next message from that entity's log (`RPOP`)
196
+ 4. Remove the entity from the ready set if its log is now empty
283
197
 
284
- ### `@QueueEntityId()`
198
+ Because Lua scripts execute atomically in Redis, the pick → gate acquisition → message pop sequence cannot be interleaved by another executor on another node. This is what eliminates race conditions — not locks, but atomicity at the Redis command level.
285
199
 
286
- Marks the property that contains the entity ID. One per class. Use this when you need per-command control over which property is the entity ID, or when you can't use the two-argument `@QueueEntity` shorthand.
200
+ ### Shared executor pool
287
201
 
288
- ```typescript
289
- @QueueEntity('account')
290
- export class TransferCommand {
291
- constructor(
292
- @QueueEntityId() public readonly accountId: string, // Routes to this account's queue
293
- public readonly targetAccountId: string,
294
- public readonly amount: number,
295
- ) {}
296
- }
297
- ```
202
+ Traditional queue systems spawn a worker per queue or per entity type. With thousands of entities, that means thousands of blocking Redis connections, thousands of event loops, and a scaling problem that grows linearly with your domain model.
203
+
204
+ atomic-queues uses a **shared executor pool** — a configurable number of concurrent executors per node that dispatch messages from *any* ready entity. One pool can service millions of distinct entities. The pool self-regulates: it drains the ready set until empty or until the concurrency limit is hit, then sleeps until the next pub/sub tickle wakes it. There are no workers to spawn, monitor, or auto-scale.
205
+
206
+ ### Gate refresh for long-running handlers
207
+
208
+ If a handler runs longer than the gate TTL, the gate doesn't expire — the executor pool runs a background interval that extends the TTL while the handler is still executing. This prevents false recovery (another node re-dispatching the same message) without requiring an unreasonably large TTL as the safety default.
209
+
210
+ ### Multiplexed result collection
211
+
212
+ Request-reply (`enqueueAndWait` / `sendAndWait`) uses a single `PSUBSCRIBE` connection per node for all concurrent result waits. Hundreds or thousands of pending results share one TCP connection to Redis, routed to the correct promise via correlation ID. No connection-per-call, no connection pool exhaustion, no subscriber amplification.
213
+
214
+ ---
215
+
216
+ ## Cross-Service Communication
217
+
218
+ This is where atomic-queues stops being a "queue library" and becomes a **distributed coordination primitive**.
219
+
220
+ ### The problem it solves
221
+
222
+ In a microservices architecture, the standard way for Service A to tell Service B to do something is: define a gRPC/REST contract, deploy an API gateway or service mesh, handle serialization, implement retries, manage circuit breakers, and hope the schema stays in sync across repos. For async communication, add a message broker (RabbitMQ, Kafka, SQS), define topic/queue naming conventions, implement dead-letter handling, and build consumer groups.
298
223
 
299
- > **Entity ID resolution order:** `@QueueEntityId()` decorator > `@QueueEntity('type', 'prop')` second argument > `@WorkerProcessor({ defaultEntityId })` > `entities[type].defaultEntityId` in module config.
224
+ atomic-queues replaces all of that with Redis.
300
225
 
301
- ### `@WorkerProcessor(options)`
226
+ ### How it works
302
227
 
303
- Optional. Define a processor class for custom job handling on top of CQRS auto-routing.
228
+ Enable the distributed registry and any service connected to the same Redis instance can send typed messages to any entity — regardless of which service owns the handler.
304
229
 
305
230
  ```typescript
306
- @WorkerProcessor({
307
- entityType: 'account',
308
- queueName: (id) => `account-${id}-queue`,
309
- workerName: (id) => `account-${id}-worker`,
310
- maxWorkersPerEntity: 1,
311
- idleTimeoutSeconds: 15,
231
+ // billing-service: defines and handles the entity
232
+ AtomicQueuesModule.forRoot({
233
+ redis: { url: process.env.REDIS_URL },
234
+ registry: { enabled: true, serviceName: 'billing-service' },
312
235
  })
313
- @Injectable()
314
- export class AccountProcessor {
315
- @JobHandler('special-audit')
316
- async handleAudit(job: Job, entityId: string) { ... }
317
- }
236
+
237
+ // payments-service: sends to it (shared Redis, no code dependency on billing)
238
+ await queueBus.enqueue(new WithdrawCommand(accountId, 100));
318
239
  ```
319
240
 
320
- ### `@JobHandler(jobName)` / `@JobHandler('*')`
241
+ When `billing-service` starts, it scans its own `@Actor`, `@CommandHandler`, and `@QueryHandler` classes and publishes **entity contracts** to Redis — a JSON document listing the entity type, accepted messages, and optional JSON schemas, refreshed via heartbeat TTL. When `payments-service` enqueues a message, the registry validates it at the call site *before* it enters the log: entity type exists, message name is accepted, payload matches schema. Errors are immediate and descriptive — not silent dead letters discovered hours later in a DLQ dashboard.
321
242
 
322
- Custom job handlers on a `@WorkerProcessor`. The wildcard `'*'` catches anything not matched by a specific handler.
243
+ ### What this replaces
323
244
 
324
- ---
245
+ Think about what you no longer need:
325
246
 
326
- ## Configuration
247
+ **No API gateway between services.** Messages go directly into the entity's log via Redis. The "endpoint" is the entity type and message name, not a URL.
248
+
249
+ **No message broker.** Redis is the transport, the ordering guarantee, and the persistence layer. You don't need RabbitMQ, Kafka, or SQS to get async cross-service communication with ordering guarantees.
250
+
251
+ **No schema registry as a separate service.** The entity contracts live in Redis alongside the message logs. Schema validation happens at the call site. Zod schemas on the producer side serialize to JSON Schema in the registry and validate on every enqueue.
252
+
253
+ **No service discovery.** The registry *is* service discovery. When a service starts, it publishes what it handles. When a service stops, its registrations TTL out. Other services discover capabilities by reading the registry.
254
+
255
+ **No serialization framework.** Messages are JSON. The wire protocol is three Redis commands. No Protobuf compilation step, no `.proto` files, no code generation from IDL. (Though atomic-queues does offer codegen from the live registry — it generates TypeScript interfaces so Service A gets compile-time type safety for messages destined to Service B, without importing Service B's code.)
256
+
257
+ **No separate dead-letter infrastructure.** Failed messages are dead-lettered per entity type in Redis, queryable via the same connection.
258
+
259
+ ### Schema validation
260
+
261
+ Attach Zod schemas to message classes for runtime safety across service boundaries:
327
262
 
328
263
  ```typescript
329
- AtomicQueuesModule.forRoot({
330
- // ── Redis connection ──────────────────────────────────────
331
- redis: {
332
- host: 'redis',
333
- port: 6379,
334
- password: 'secret', // optional
335
- },
264
+ import { Schema } from 'atomic-queues';
265
+ import { z } from 'zod';
266
+
267
+ @Schema(z.object({
268
+ accountId: z.string().uuid(),
269
+ amount: z.number().positive(),
270
+ }))
271
+ @EntityType('account')
272
+ export class WithdrawCommand {
273
+ @QueueEntityId() public readonly accountId: string;
274
+ public readonly amount: number;
275
+ }
276
+ ```
336
277
 
337
- // ── Global settings ───────────────────────────────────────
338
- keyPrefix: 'myapp', // Redis key namespace (default: 'aq')
339
- enableCronManager: true, // Legacy cron-based scaling (optional)
340
- cronInterval: 5000, // Cron tick interval in ms
341
-
342
- // ── Worker defaults ───────────────────────────────────────
343
- workerDefaults: {
344
- concurrency: 1, // Jobs processed concurrently per worker
345
- stalledInterval: 1000, // ms between stalled-job checks
346
- lockDuration: 30000, // ms a job is locked during processing
347
- heartbeatTTL: 3, // Heartbeat key TTL in seconds
348
- },
278
+ The Zod schema serializes to JSON Schema and stores in the registry. Every service validates payloads against it — even services that don't import your code, even services written in a different language that read the registry directly from Redis.
349
279
 
350
- // ── Per-entity configuration (optional) ───────────────────
351
- entities: {
352
- account: {
353
- queueName: (id) => `account-${id}-queue`,
354
- workerName: (id) => `account-${id}-worker`,
355
- maxWorkersPerEntity: 1,
356
- idleTimeoutSeconds: 15,
357
-
358
- // Fallback property name for entity ID extraction.
359
- // Used when a command has no @QueueEntityId() decorator
360
- // and no second argument to @QueueEntity().
361
- defaultEntityId: 'accountId',
280
+ ### Entity co-ownership
362
281
 
363
- workerConfig: { // Override workerDefaults per entity
364
- concurrency: 1,
365
- lockDuration: 60000,
366
- },
367
- },
368
- },
369
- });
282
+ Multiple services can handle different message types on the same entity. Service A handles `DepositCommand` and `WithdrawCommand` on the `account` entity type. Service B handles `FreezeAccountCommand` on the same entity type. The registry merges their contracts automatically. The dispatch gate still ensures single-writer semantics per entity instance, regardless of which service's executor picks up the message.
283
+
284
+ ### Contract codegen
285
+
286
+ Generate typed interfaces from the live registry:
287
+
288
+ ```bash
289
+ REDIS_URL=redis://localhost:6379 npx atomic-queues generate --ts --output ./generated/contracts.ts
370
290
  ```
371
291
 
292
+ Also supports `--json-schema` for language-agnostic schema export and `--snapshot` for full registry dumps.
293
+
372
294
  ---
373
295
 
374
- ## Distributed Worker Lifecycle
296
+ ## Redis *is* the Protocol
297
+
298
+ This is the most important architectural decision in the project, and it has implications that go far beyond NestJS.
375
299
 
376
- Workers in atomic-queues have a fully automated lifecycle, distributed across all pods with no leader election:
300
+ The wire protocol is [fully documented](./WIRE-PROTOCOL.md), intentionally simple, and versioned with breaking-change semantics. Enqueuing a message is three Redis commands:
377
301
 
378
302
  ```
379
- Job arrives SET NX claim
380
- on any pod ──────► ┌──────────────────────┐
381
- Pod claims worker? │
382
- └──────┬───────┬───────┘
383
- YES │ │ NO (another pod won)
384
- ▼ ▼
385
- ┌────────┐ ┌──────────────┐
386
- │ Spawn │ │ Wait — other │
387
- │ worker │ │ pod handles │
388
- │ locally│ └──────────────┘
389
- └───┬────┘
390
-
391
- ┌──────────────┐
392
- │ Processing │◄──── Heartbeat refresh (pipeline)
393
- │ jobs back- │ every 1s (1 Redis round-trip)
394
- │ to-back │
395
- └──────┬───────┘
396
- │ No jobs for idleTimeoutSeconds
397
-
398
- ┌──────────────┐
399
- │ Idle sweep │──── Hot cache eviction
400
- │ closes │ Heartbeat keys cleaned up
401
- │ worker │
402
- └──────────────┘
303
+ LPUSH aq:log:account:a-1 '<message JSON>'
304
+ SADD aq:ready account:a-1
305
+ PUBLISH aq:tickle 1
403
306
  ```
404
307
 
405
- ### Hot Cache
308
+ **Any language with a Redis client is a first-class citizen.** A Python data pipeline can enqueue commands to a NestJS-hosted actor. A Go microservice can fire events at entities defined in TypeScript. A Rust executor can run the same Lua scheduling script and compete for gates on equal terms with the Node.js executor pool. A Bash script can trigger a workflow.
406
309
 
407
- After a worker is confirmed alive, subsequent job arrivals for that entity hit an **in-memory cache** zero Redis calls on the warm path. This eliminates the per-job Redis overhead that plagues lock-based approaches.
310
+ This is not a feature of any existing mainstream actor framework. Orleans requires the Orleans silo. Akka requires the JVM. Temporal requires the Temporal server with its own database. All of them are monoglot execution environments actors must be written in the framework's language.
408
311
 
409
- | Path | Redis calls | When |
410
- |---|---|---|
411
- | **Hot** (cache hit) | 0 | Worker known alive |
412
- | **Warm** (cache miss) | 1 (`EXISTS`) | First time seeing entity |
413
- | **Cold** (no worker) | 1 (`SET NX`) | Worker needs creation |
312
+ atomic-queues is **polyglot by construction**. The coordination happens in Redis, not in the runtime. Any process that speaks the wire protocol participates on equal terms, and the [WIRE-PROTOCOL.md](./WIRE-PROTOCOL.md) includes a complete Python reference client to prove it.
414
313
 
415
- ### SpawnQueueService
314
+ This opens architectures that are genuinely difficult to build otherwise:
416
315
 
417
- For multi-pod deployments, the `SpawnQueueService` distributes worker creation across all pods via a shared BullMQ spawn queue. The **direct local spawn** path bypasses this queue entirely — the pod that first sees a job for a new entity claims it with an atomic `SET NX` and spawns the worker locally.
316
+ - **Ingest in Go, process in Node.js, analyze in Python.** Each layer speaks Redis. The entity logs are the integration boundary.
317
+ - **Rust executors for CPU-hot-path actors.** The same Lua scheduler, the same gates, the same entity logs. The Rust process is just another executor that happens to be faster. The Node.js side doesn't know or care.
318
+ - **Gradual migration.** Move one entity type's handlers to a different service, a different language, or a different infrastructure — without touching any other service's code. The entity contract in the registry is the interface, not the import statement.
319
+ - **Edge coordination.** An IoT device with a Redis client and 3 commands of knowledge can participate in the same entity model as your cloud services.
418
320
 
419
321
  ---
420
322
 
421
- ## Complete Example
422
-
423
- A banking service with withdrawals, deposits, and cross-account transfers:
323
+ ## Quick Start
424
324
 
425
325
  ```typescript
426
- // ── Module ──────────────────────────────────────────────
427
326
  import { Module } from '@nestjs/common';
428
- import { CqrsModule } from '@nestjs/cqrs';
429
327
  import { AtomicQueuesModule } from 'atomic-queues';
430
328
 
431
329
  @Module({
432
330
  imports: [
433
- CqrsModule,
434
331
  AtomicQueuesModule.forRoot({
435
- redis: { host: 'redis', port: 6379 },
436
- keyPrefix: 'banking',
437
- entities: {
438
- account: {
439
- queueName: (id) => `account-${id}-queue`,
440
- workerName: (id) => `account-${id}-worker`,
441
- maxWorkersPerEntity: 1,
442
- idleTimeoutSeconds: 15,
443
- },
444
- },
332
+ redis: { host: 'localhost', port: 6379 },
445
333
  }),
446
334
  ],
447
- providers: [
448
- AccountService,
449
- WithdrawHandler,
450
- DepositHandler,
451
- TransferHandler,
452
- ],
453
335
  })
454
- export class BankingModule {}
336
+ export class AppModule {}
337
+ ```
455
338
 
456
- // ── Commands ────────────────────────────────────────────
457
- import { QueueEntity, QueueEntityId } from 'atomic-queues';
339
+ Define a command and enqueue it:
458
340
 
459
- @QueueEntity('account')
341
+ ```typescript
342
+ @EntityType('account')
460
343
  export class WithdrawCommand {
461
344
  constructor(
462
345
  @QueueEntityId() public readonly accountId: string,
463
346
  public readonly amount: number,
464
- public readonly transactionId: string,
465
347
  ) {}
466
348
  }
467
349
 
468
- @QueueEntity('account')
469
- export class DepositCommand {
470
- constructor(
471
- @QueueEntityId() public readonly accountId: string,
472
- public readonly amount: number,
473
- public readonly source: string,
474
- ) {}
475
- }
350
+ @Injectable()
351
+ export class PaymentService {
352
+ constructor(private readonly queueBus: QueueBus) {}
476
353
 
477
- @QueueEntity('account')
478
- export class TransferCommand {
479
- constructor(
480
- @QueueEntityId() public readonly accountId: string,
481
- public readonly toAccountId: string,
482
- public readonly amount: number,
483
- ) {}
354
+ async withdraw(accountId: string, amount: number) {
355
+ await this.queueBus.enqueue(new WithdrawCommand(accountId, amount));
356
+ }
484
357
  }
358
+ ```
485
359
 
486
- // ── Handlers ────────────────────────────────────────────
487
- import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
360
+ The command is appended to `account:{accountId}`'s message log and executed sequentially by the shared executor pool. No handler registration, no worker setup, no queue configuration.
488
361
 
489
- @CommandHandler(WithdrawCommand)
490
- export class WithdrawHandler implements ICommandHandler<WithdrawCommand> {
491
- constructor(private readonly repo: AccountRepository) {}
362
+ ---
492
363
 
493
- async execute({ accountId, amount }: WithdrawCommand) {
494
- const account = await this.repo.findById(accountId);
495
- if (account.balance < amount) throw new InsufficientFundsError();
496
- account.balance -= amount;
497
- await this.repo.save(account);
498
- }
499
- }
364
+ ## Configuration
500
365
 
501
- @CommandHandler(TransferCommand)
502
- export class TransferHandler implements ICommandHandler<TransferCommand> {
503
- constructor(
504
- private readonly repo: AccountRepository,
505
- private readonly queueBus: QueueBus,
506
- ) {}
366
+ ```typescript
367
+ AtomicQueuesModule.forRoot({
368
+ redis: { host: 'localhost', port: 6379 },
507
369
 
508
- async execute({ accountId, toAccountId, amount }: TransferCommand) {
509
- // Debit source (we're in source account's queue — safe)
510
- const source = await this.repo.findById(accountId);
511
- if (source.balance < amount) throw new InsufficientFundsError();
512
- source.balance -= amount;
513
- await this.repo.save(source);
514
-
515
- // Credit destination (enqueued to destination's queue — also safe)
516
- await this.queueBus.enqueue(
517
- new DepositCommand(toAccountId, amount, `transfer:${accountId}`),
518
- );
519
- }
520
- }
370
+ executor: {
371
+ poolSize: 1, // concurrent executors per node
372
+ gateTTL: 30, // seconds before gate expires (safety net)
373
+ },
521
374
 
522
- // ── Controller ──────────────────────────────────────────
523
- import { Controller, Post, Body, Param } from '@nestjs/common';
524
- import { QueueBus } from 'atomic-queues';
375
+ entities: {
376
+ account: {
377
+ defaultEntityId: 'accountId',
378
+ gateTTL: 60,
379
+ retry: { maxAttempts: 5, backoff: 'exponential', backoffDelay: 2000 },
380
+ actorIdleTimeout: 120000,
381
+ statePersistence: true,
382
+ },
383
+ },
525
384
 
526
- @Controller('accounts')
527
- export class AccountController {
528
- constructor(private readonly queueBus: QueueBus) {}
385
+ registry: {
386
+ enabled: false,
387
+ serviceName: 'my-service',
388
+ schemaValidation: false,
389
+ heartbeatInterval: 10000,
390
+ registrationTTL: 30,
391
+ },
529
392
 
530
- @Post(':id/withdraw')
531
- async withdraw(@Param('id') id: string, @Body() body: { amount: number }) {
532
- await this.queueBus.enqueue(new WithdrawCommand(id, body.amount, uuid()));
533
- return { queued: true };
534
- }
393
+ keyPrefix: 'aq',
394
+ verbose: false,
395
+ })
396
+ ```
535
397
 
536
- @Post(':id/transfer')
537
- async transfer(
538
- @Param('id') id: string,
539
- @Body() body: { to: string; amount: number },
540
- ) {
541
- await this.queueBus.enqueue(new TransferCommand(id, body.to, body.amount));
542
- return { queued: true };
543
- }
544
- }
398
+ Optional peer dependencies:
399
+
400
+ ```bash
401
+ npm install @nestjs/cqrs # for CQRS handler auto-wiring
402
+ npm install zod zod-to-json-schema # for schema validation in the registry
545
403
  ```
546
404
 
547
405
  ---
548
406
 
549
- ## Advanced: Custom Worker Processors
407
+ ## Guarantees
550
408
 
551
- For cases where CQRS auto-routing isn't enough, define a `@WorkerProcessor` with explicit `@JobHandler` methods:
552
-
553
- ```typescript
554
- import { Injectable } from '@nestjs/common';
555
- import { WorkerProcessor, JobHandler } from 'atomic-queues';
556
- import { Job } from 'bullmq';
557
-
558
- @WorkerProcessor({
559
- entityType: 'account',
560
- queueName: (id) => `account-${id}-queue`,
561
- workerName: (id) => `account-${id}-worker`,
562
- maxWorkersPerEntity: 1,
563
- idleTimeoutSeconds: 15,
564
- })
565
- @Injectable()
566
- export class AccountProcessor {
567
- @JobHandler('high-priority-audit')
568
- async handleAudit(job: Job, entityId: string) {
569
- // Specific handler for this job type
570
- }
409
+ | Guarantee | Scope | Mechanism |
410
+ |---|---|---|
411
+ | FIFO per entity | Cluster-wide | Redis list (`LPUSH`/`RPOP`) |
412
+ | Single-writer per entity | Cluster-wide | Gate key (`SET NX EX`) |
413
+ | At-least-once delivery | Per message | Retry on gate TTL expiry |
414
+ | Parallel across entities | Per node | Executor pool concurrency |
415
+ | Durability | Per message | Redis persistence (AOF/RDB) |
571
416
 
572
- @JobHandler('*')
573
- async handleAll(job: Job, entityId: string) {
574
- // Wildcard — catches everything not explicitly handled
575
- // Falls back to CQRS routing automatically when not defined
576
- }
577
- }
578
- ```
417
+ ### What this does NOT guarantee
579
418
 
580
- > **Priority order:** Explicit `@JobHandler` CQRS auto-routing (`@JobCommand`/`@JobQuery`) Wildcard handler
419
+ **Exactly-once processing.** Like every distributed message system — Orleans, Akka, Temporal, Kafka — handlers must be idempotent. If an executor crashes mid-processing, the gate TTL expires and the message retries on another node. This is a fundamental constraint of distributed systems, not a limitation of the library.
581
420
 
582
421
  ---
583
422
 
584
- ## Performance
423
+ ## How It Compares
585
424
 
586
- ### Why it's fast
425
+ | Capability | BullMQ | Temporal | atomic-queues |
426
+ |---|---|---|---|
427
+ | Per-entity ordering | Manual (named queues) | Workflow-scoped | Built-in, zero config |
428
+ | Cross-entity parallelism | Worker pools | Worker pools | Shared executor pool |
429
+ | Stateful entities | No | Workflow state | Virtual actors |
430
+ | Cross-service messaging | Shared queue names | gRPC | Redis registry + codegen |
431
+ | Polyglot clients | JS/TS only | SDK per language | Any Redis client (3 commands) |
432
+ | Infrastructure required | Redis | Temporal server + DB | Redis only |
433
+ | Distributed locks needed | Yes, for ordering | Internal | None — gates are non-contending |
434
+ | Service discovery | External | Built-in | Built-in (registry) |
435
+ | Schema validation | No | Protobuf | Zod → JSON Schema |
587
436
 
588
- 1. **Zero contention** — no locks, no retries, no backoff. Jobs queue and execute.
589
- 2. **Hot cache** — after first check, subsequent job arrivals for an entity incur 0 Redis calls.
590
- 3. **Direct local spawn** — atomic `SET NX` claim, local worker creation. No queue round-trip.
591
- 4. **Pipelined heartbeats** — heartbeat refresh uses a single Redis pipeline (1 round-trip for 2 keys).
592
- 5. **O(1) worker existence check** — global alive key replaces `KEYS` pattern scan.
437
+ ---
593
438
 
594
- ### When to use what
439
+ ## Decorator Reference
595
440
 
596
- | Use case | Recommendation |
441
+ | Decorator | Purpose |
597
442
  |---|---|
598
- | Per-entity operations that must be serialized (payments, inventory, game state) | **atomic-queues** |
599
- | Rare, low-frequency mutual exclusion (config updates, migrations) | Redlock / advisory locks |
600
- | Exactly-once semantics with audit trail | **atomic-queues** (BullMQ job IDs) |
601
- | Sub-millisecond synchronous response required | Redlock (synchronous acquire) |
602
- | Multi-pod, many entities, sustained load | **atomic-queues** (contention-free scaling) |
443
+ | `@EntityType('type')` | Route a message to an entity type |
444
+ | `@QueueEntityId()` | Mark the property holding the entity ID |
445
+ | `@QueueEntity('type', 'prop')` | Combined entity type + ID |
446
+ | `@Actor('type')` | Declare a virtual actor class |
447
+ | `@On(MessageClass)` | Handle a message type on an actor |
448
+ | `@Schema(zodSchema)` | Attach a Zod schema for registry validation |
449
+
450
+ ---
451
+
452
+ ## Migrating from V1
453
+
454
+ V2 is a full rewrite of the internals. BullMQ is removed. Workers are removed. The public API is largely preserved.
455
+
456
+ **What stays the same**: `@EntityType`, `@QueueEntityId`, `@QueueEntity`, `queueBus.enqueue()`, `queueBus.forEntity()`, `queueBus.enqueueAndWait()`.
457
+
458
+ **What's removed**: `@WorkerProcessor`, `@JobHandler`, `@EntityScaler`, `@OnSpawnWorker`, `@OnTerminateWorker`, `@GetActiveEntities`, `@GetDesiredWorkerCount`, `.forProcessor()`. All worker and scaling concepts are gone.
459
+
460
+ **What's new**: `@Actor`, `@On`, `@Schema`, `ActorSystem`, `RegistryService`, distributed registry, codegen CLI.
461
+
462
+ **Migration steps**: (1) remove all `@WorkerProcessor` classes — replace with `@Actor` or configure entity defaults in module config; (2) remove all scaling decorators; (3) run the data migration script to drain in-flight BullMQ jobs to the new log format; (4) remove `bullmq` and `@nestjs/bullmq` from your dependencies.
603
463
 
604
464
  ---
605
465