atomic-queues 2.2.0 → 3.0.0

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 (236) hide show
  1. package/README.md +296 -417
  2. package/dist/cli/generators/classes.d.ts +1 -1
  3. package/dist/cli/generators/json-schema.d.ts +1 -1
  4. package/dist/cli/generators/typescript.d.ts +1 -1
  5. package/dist/cli/index.js +147 -5
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cluster/cluster-discovery.service.d.ts +91 -0
  8. package/dist/cluster/cluster-discovery.service.d.ts.map +1 -0
  9. package/dist/cluster/cluster-discovery.service.js +423 -0
  10. package/dist/cluster/cluster-discovery.service.js.map +1 -0
  11. package/dist/cluster/grpc-peer-monitor.service.d.ts +31 -0
  12. package/dist/cluster/grpc-peer-monitor.service.d.ts.map +1 -0
  13. package/dist/cluster/grpc-peer-monitor.service.js +192 -0
  14. package/dist/cluster/grpc-peer-monitor.service.js.map +1 -0
  15. package/dist/cluster/index.d.ts +7 -0
  16. package/dist/cluster/index.d.ts.map +1 -0
  17. package/dist/cluster/index.js +23 -0
  18. package/dist/cluster/index.js.map +1 -0
  19. package/dist/cluster/leader-election.service.d.ts +38 -0
  20. package/dist/cluster/leader-election.service.d.ts.map +1 -0
  21. package/dist/cluster/leader-election.service.js +184 -0
  22. package/dist/cluster/leader-election.service.js.map +1 -0
  23. package/dist/cluster/master-coordinator.d.ts +50 -0
  24. package/dist/cluster/master-coordinator.d.ts.map +1 -0
  25. package/dist/cluster/master-coordinator.js +307 -0
  26. package/dist/cluster/master-coordinator.js.map +1 -0
  27. package/dist/cluster/redis-health-monitor.service.d.ts +23 -0
  28. package/dist/cluster/redis-health-monitor.service.d.ts.map +1 -0
  29. package/dist/cluster/redis-health-monitor.service.js +100 -0
  30. package/dist/cluster/redis-health-monitor.service.js.map +1 -0
  31. package/dist/cluster/server-ring.service.d.ts +48 -0
  32. package/dist/cluster/server-ring.service.d.ts.map +1 -0
  33. package/dist/cluster/server-ring.service.js +136 -0
  34. package/dist/cluster/server-ring.service.js.map +1 -0
  35. package/dist/decorators/constants.d.ts +0 -3
  36. package/dist/decorators/constants.d.ts.map +1 -1
  37. package/dist/decorators/constants.js +1 -5
  38. package/dist/decorators/constants.js.map +1 -1
  39. package/dist/decorators/entity.decorators.d.ts +16 -24
  40. package/dist/decorators/entity.decorators.d.ts.map +1 -1
  41. package/dist/decorators/entity.decorators.js +0 -39
  42. package/dist/decorators/entity.decorators.js.map +1 -1
  43. package/dist/decorators/index.d.ts +0 -1
  44. package/dist/decorators/index.d.ts.map +1 -1
  45. package/dist/decorators/index.js +0 -1
  46. package/dist/decorators/index.js.map +1 -1
  47. package/dist/decorators/interfaces.d.ts +10 -28
  48. package/dist/decorators/interfaces.d.ts.map +1 -1
  49. package/dist/decorators/job.decorators.d.ts +4 -52
  50. package/dist/decorators/job.decorators.d.ts.map +1 -1
  51. package/dist/decorators/job.decorators.js +6 -54
  52. package/dist/decorators/job.decorators.js.map +1 -1
  53. package/dist/decorators/metadata-readers.d.ts +5 -5
  54. package/dist/decorators/metadata-readers.d.ts.map +1 -1
  55. package/dist/decorators/metadata-readers.js +2 -8
  56. package/dist/decorators/metadata-readers.js.map +1 -1
  57. package/dist/decorators/schema.decorators.d.ts +1 -1
  58. package/dist/decorators/schema.decorators.d.ts.map +1 -1
  59. package/dist/decorators/schema.decorators.js.map +1 -1
  60. package/dist/decorators/utils.d.ts +1 -1
  61. package/dist/decorators/utils.d.ts.map +1 -1
  62. package/dist/decorators/utils.js +5 -1
  63. package/dist/decorators/utils.js.map +1 -1
  64. package/dist/domain/interfaces/config.interfaces.d.ts +92 -35
  65. package/dist/domain/interfaces/config.interfaces.d.ts.map +1 -1
  66. package/dist/domain/interfaces/index.d.ts +1 -0
  67. package/dist/domain/interfaces/index.d.ts.map +1 -1
  68. package/dist/domain/interfaces/index.js +1 -0
  69. package/dist/domain/interfaces/index.js.map +1 -1
  70. package/dist/{services/registry → domain/interfaces}/registry.types.d.ts.map +1 -1
  71. package/dist/domain/interfaces/registry.types.js.map +1 -0
  72. package/dist/grpc/grpc-client-pool.service.d.ts +71 -0
  73. package/dist/grpc/grpc-client-pool.service.d.ts.map +1 -0
  74. package/dist/grpc/grpc-client-pool.service.js +307 -0
  75. package/dist/grpc/grpc-client-pool.service.js.map +1 -0
  76. package/dist/grpc/grpc-server.service.d.ts +47 -0
  77. package/dist/grpc/grpc-server.service.d.ts.map +1 -0
  78. package/dist/grpc/grpc-server.service.js +494 -0
  79. package/dist/grpc/grpc-server.service.js.map +1 -0
  80. package/dist/grpc/index.d.ts +3 -0
  81. package/dist/grpc/index.d.ts.map +1 -0
  82. package/dist/{services/gate → grpc}/index.js +2 -1
  83. package/dist/grpc/index.js.map +1 -0
  84. package/dist/index.d.ts +4 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +4 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/module/atomic-queues.module.d.ts +1 -0
  89. package/dist/module/atomic-queues.module.d.ts.map +1 -1
  90. package/dist/module/atomic-queues.module.js +60 -11
  91. package/dist/module/atomic-queues.module.js.map +1 -1
  92. package/dist/services/command-discovery/command-discovery.service.js +2 -2
  93. package/dist/services/command-discovery/command-discovery.service.js.map +1 -1
  94. package/dist/services/entity-type-registry/entity-type-registry.service.d.ts +13 -0
  95. package/dist/services/entity-type-registry/entity-type-registry.service.d.ts.map +1 -0
  96. package/dist/services/entity-type-registry/entity-type-registry.service.js +75 -0
  97. package/dist/services/entity-type-registry/entity-type-registry.service.js.map +1 -0
  98. package/dist/services/entity-type-registry/index.d.ts +2 -0
  99. package/dist/services/entity-type-registry/index.d.ts.map +1 -0
  100. package/dist/services/{actor-system → entity-type-registry}/index.js +1 -1
  101. package/dist/services/entity-type-registry/index.js.map +1 -0
  102. package/dist/services/handler-executor/handler-executor.service.d.ts +0 -2
  103. package/dist/services/handler-executor/handler-executor.service.d.ts.map +1 -1
  104. package/dist/services/handler-executor/handler-executor.service.js +0 -19
  105. package/dist/services/handler-executor/handler-executor.service.js.map +1 -1
  106. package/dist/services/index.d.ts +3 -9
  107. package/dist/services/index.d.ts.map +1 -1
  108. package/dist/services/index.js +3 -9
  109. package/dist/services/index.js.map +1 -1
  110. package/dist/services/message-router/index.d.ts +2 -0
  111. package/dist/services/message-router/index.d.ts.map +1 -0
  112. package/dist/services/{actor-registry → message-router}/index.js +1 -1
  113. package/dist/services/message-router/index.js.map +1 -0
  114. package/dist/services/message-router/message-router.service.d.ts +53 -0
  115. package/dist/services/message-router/message-router.service.d.ts.map +1 -0
  116. package/dist/services/message-router/message-router.service.js +519 -0
  117. package/dist/services/message-router/message-router.service.js.map +1 -0
  118. package/dist/services/queue-bus/cluster-contracts.d.ts +1 -1
  119. package/dist/services/queue-bus/cluster-contracts.d.ts.map +1 -1
  120. package/dist/services/queue-bus/cluster-contracts.js.map +1 -1
  121. package/dist/services/queue-bus/queue-bus.service.d.ts +3 -21
  122. package/dist/services/queue-bus/queue-bus.service.d.ts.map +1 -1
  123. package/dist/services/queue-bus/queue-bus.service.js +15 -119
  124. package/dist/services/queue-bus/queue-bus.service.js.map +1 -1
  125. package/dist/utils/id.utils.d.ts +3 -0
  126. package/dist/utils/id.utils.d.ts.map +1 -0
  127. package/dist/utils/id.utils.js +14 -0
  128. package/dist/utils/id.utils.js.map +1 -0
  129. package/dist/utils/index.d.ts +1 -0
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/index.js +1 -0
  132. package/dist/utils/index.js.map +1 -1
  133. package/dist/wal/index.d.ts +4 -0
  134. package/dist/wal/index.d.ts.map +1 -0
  135. package/dist/{services/executor-pool → wal}/index.js +3 -1
  136. package/dist/wal/index.js.map +1 -0
  137. package/dist/wal/wal.scripts.d.ts +51 -0
  138. package/dist/wal/wal.scripts.d.ts.map +1 -0
  139. package/dist/wal/wal.scripts.js +84 -0
  140. package/dist/wal/wal.scripts.js.map +1 -0
  141. package/dist/wal/wal.service.d.ts +46 -0
  142. package/dist/wal/wal.service.d.ts.map +1 -0
  143. package/dist/wal/wal.service.js +243 -0
  144. package/dist/wal/wal.service.js.map +1 -0
  145. package/dist/wal/wal.types.d.ts +23 -0
  146. package/dist/wal/wal.types.d.ts.map +1 -0
  147. package/dist/wal/wal.types.js +3 -0
  148. package/dist/wal/wal.types.js.map +1 -0
  149. package/dist/workers/consistent-hash.d.ts +97 -0
  150. package/dist/workers/consistent-hash.d.ts.map +1 -0
  151. package/dist/workers/consistent-hash.js +231 -0
  152. package/dist/workers/consistent-hash.js.map +1 -0
  153. package/dist/workers/entity-worker-manager.d.ts +35 -0
  154. package/dist/workers/entity-worker-manager.d.ts.map +1 -0
  155. package/dist/workers/entity-worker-manager.js +237 -0
  156. package/dist/workers/entity-worker-manager.js.map +1 -0
  157. package/dist/workers/entity-worker.d.ts +54 -0
  158. package/dist/workers/entity-worker.d.ts.map +1 -0
  159. package/dist/workers/entity-worker.js +142 -0
  160. package/dist/workers/entity-worker.js.map +1 -0
  161. package/dist/workers/index.d.ts +4 -0
  162. package/dist/workers/index.d.ts.map +1 -0
  163. package/dist/workers/index.js +20 -0
  164. package/dist/workers/index.js.map +1 -0
  165. package/package.json +17 -4
  166. package/dist/decorators/actor.decorators.d.ts +0 -4
  167. package/dist/decorators/actor.decorators.d.ts.map +0 -1
  168. package/dist/decorators/actor.decorators.js +0 -32
  169. package/dist/decorators/actor.decorators.js.map +0 -1
  170. package/dist/services/actor-registry/actor-registry.service.d.ts +0 -32
  171. package/dist/services/actor-registry/actor-registry.service.d.ts.map +0 -1
  172. package/dist/services/actor-registry/actor-registry.service.js +0 -220
  173. package/dist/services/actor-registry/actor-registry.service.js.map +0 -1
  174. package/dist/services/actor-registry/index.d.ts +0 -2
  175. package/dist/services/actor-registry/index.d.ts.map +0 -1
  176. package/dist/services/actor-registry/index.js.map +0 -1
  177. package/dist/services/actor-system/actor-system.service.d.ts +0 -19
  178. package/dist/services/actor-system/actor-system.service.d.ts.map +0 -1
  179. package/dist/services/actor-system/actor-system.service.js +0 -86
  180. package/dist/services/actor-system/actor-system.service.js.map +0 -1
  181. package/dist/services/actor-system/index.d.ts +0 -2
  182. package/dist/services/actor-system/index.d.ts.map +0 -1
  183. package/dist/services/actor-system/index.js.map +0 -1
  184. package/dist/services/executor-pool/executor-pool.service.d.ts +0 -38
  185. package/dist/services/executor-pool/executor-pool.service.d.ts.map +0 -1
  186. package/dist/services/executor-pool/executor-pool.service.js +0 -180
  187. package/dist/services/executor-pool/executor-pool.service.js.map +0 -1
  188. package/dist/services/executor-pool/index.d.ts +0 -2
  189. package/dist/services/executor-pool/index.d.ts.map +0 -1
  190. package/dist/services/executor-pool/index.js.map +0 -1
  191. package/dist/services/gate/gate.service.d.ts +0 -17
  192. package/dist/services/gate/gate.service.d.ts.map +0 -1
  193. package/dist/services/gate/gate.service.js +0 -81
  194. package/dist/services/gate/gate.service.js.map +0 -1
  195. package/dist/services/gate/index.d.ts +0 -2
  196. package/dist/services/gate/index.d.ts.map +0 -1
  197. package/dist/services/gate/index.js.map +0 -1
  198. package/dist/services/log/index.d.ts +0 -2
  199. package/dist/services/log/index.d.ts.map +0 -1
  200. package/dist/services/log/index.js +0 -18
  201. package/dist/services/log/index.js.map +0 -1
  202. package/dist/services/log/log.service.d.ts +0 -21
  203. package/dist/services/log/log.service.d.ts.map +0 -1
  204. package/dist/services/log/log.service.js +0 -92
  205. package/dist/services/log/log.service.js.map +0 -1
  206. package/dist/services/registry/index.d.ts +0 -4
  207. package/dist/services/registry/index.d.ts.map +0 -1
  208. package/dist/services/registry/index.js +0 -20
  209. package/dist/services/registry/index.js.map +0 -1
  210. package/dist/services/registry/registry.service.d.ts +0 -43
  211. package/dist/services/registry/registry.service.d.ts.map +0 -1
  212. package/dist/services/registry/registry.service.js +0 -402
  213. package/dist/services/registry/registry.service.js.map +0 -1
  214. package/dist/services/registry/registry.types.js.map +0 -1
  215. package/dist/services/registry/schema-converter.d.ts +0 -2
  216. package/dist/services/registry/schema-converter.d.ts.map +0 -1
  217. package/dist/services/registry/schema-converter.js +0 -27
  218. package/dist/services/registry/schema-converter.js.map +0 -1
  219. package/dist/services/result-collector/index.d.ts +0 -2
  220. package/dist/services/result-collector/index.d.ts.map +0 -1
  221. package/dist/services/result-collector/index.js +0 -18
  222. package/dist/services/result-collector/index.js.map +0 -1
  223. package/dist/services/result-collector/result-collector.service.d.ts +0 -17
  224. package/dist/services/result-collector/result-collector.service.d.ts.map +0 -1
  225. package/dist/services/result-collector/result-collector.service.js +0 -92
  226. package/dist/services/result-collector/result-collector.service.js.map +0 -1
  227. package/dist/services/scheduler/index.d.ts +0 -2
  228. package/dist/services/scheduler/index.d.ts.map +0 -1
  229. package/dist/services/scheduler/index.js +0 -18
  230. package/dist/services/scheduler/index.js.map +0 -1
  231. package/dist/services/scheduler/scheduler.service.d.ts +0 -17
  232. package/dist/services/scheduler/scheduler.service.d.ts.map +0 -1
  233. package/dist/services/scheduler/scheduler.service.js +0 -140
  234. package/dist/services/scheduler/scheduler.service.js.map +0 -1
  235. /package/dist/{services/registry → domain/interfaces}/registry.types.d.ts +0 -0
  236. /package/dist/{services/registry → domain/interfaces}/registry.types.js +0 -0
package/README.md CHANGED
@@ -10,11 +10,11 @@
10
10
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣽⣟⣳⡝⡼⢁⠎⠀⡀⢁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡄⠰⣄⠈⠓⢌⠛⢽⣣⡟⢿⠿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿█▀█ █ █▄█ █ ▀ █ █ █▄▄
11
11
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡿⣽⠳⡼⢁⡞⠀⡜⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⢸⢵⠀⠀⠁⠂⠤⣉⠉⠓⠒⠚⠦⠥⡈⠉⣙⢛⡿⣿█▀█ █ █ █▀▀ █ █ █▀▀ █▀▀
12
12
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡾⣽⣏⢳⢃⣞⠃⡼⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠁⢀⣀⠤⠐⢋⡰⣌⣾⣿⣿▀▀█ █▄█ ██▄ █▄█ ██▄ ▄▄█
13
- ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣮⢳⣿⠶⠁⠖⠃⠀⠁⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠟⠛⠛⠀⠀⠀⠀⢀⡤⠤⠐⠒⣉⠡⣄⠶⣭⣿⣽⣿⣿⣿⣿
14
- ⣿⣿⣿⣿⣿⣿⣿⡿⠿⢉⡢⠝⠁⠀⠃⠀⠀⠀⠀⠀⠿⠃⠿⠿⠿⠛⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⣀⢤⣰⣲⣽⣾⡟⣾⣿⣿⣿⣿⣿⣿⣿⣿
15
- ⣿⣿⣟⡿⡚⠏⠁⠀⠐⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠂⣠⠀⣯⣗⣮⢿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e r o c o n t e n t i o n
16
- ⣿⢯⡝⠠⠁⠀⠀⠠⠤⠀⠀⠀⠀⡀⠢⣄⣀⡀⠐⠤⡀⠀⠀⠀⢤⣄⣀⠤⣄⣤⢤⣖⡾⠋⢁⡼⠁⣸⡿⣞⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿p e r e n t i t y
17
- ⣿⣷⣾⣵⣦⣶⣖⣳⣶⣝⣶⣯⣷⣽⣷⣾⣶⣽⣯⣶⠄⠈⠒⣤⣀⠉⠙⠛⠛⠋⠋⢁⣠⠔⠁⠀⢰⣿⣽⣯⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿l o c k f r e e
13
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣮⢳⣿⠶⠁⠖⠃⠀⠁⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠟⠛⠛⠀⠀⠀⠀⢀⡤⠤⠐⠒⣉⠡⣄⠶⣭⣿⣽⣿⣿⣿⣿⣿
14
+ ⣿⣿⣿⣿⣿⣿⣿⡿⠿⢉⡢⠝⠁⠀⠃⠀⠀⠀⠀⠀⠿⠃⠿⠿⠿⠛⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⣀⢤⣰⣲⣽⣾⡟⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿
15
+ ⣿⣟⡿⡚⠏⠁⠀⠀⠐⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠂⣠⠀⣯⣗⣮⢿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿v i r t u a l a c t o r s
16
+ ⣿⢯⡝⠠⠁⠀⠀⠠⠤⠀⠀⠀⠀⡀⠢⣄⣀⡀⠐⠤⡀⠀⠀⠀⢤⣄⣀⠤⣄⣤⢤⣖⡾⠋⢁⡼⠁⣸⡿⣞⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿s t r i c t l y o n c e
17
+ ⣿⣷⣾⣵⣦⣶⣖⣳⣶⣝⣶⣯⣷⣽⣷⣾⣶⣽⣯⣶⠄⠈⠒⣤⣀⠉⠙⠛⠛⠋⠋⢁⣠⠔⠁⠀⢰⣿⣽⣯⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿z e r o l o c k s
18
18
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡄⡀⡉⠛⠓⠶⠶⠒⠛⠋⠀⠀⢀⣼⣻⢷⣾⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
19
19
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣧⡵⣌⣒⢂⠀⣀⣀⣠⣤⣶⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
20
20
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣾⣷⣯⣿⣧⣿⣷⣿⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
@@ -26,7 +26,6 @@
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/Redis-7-dc382d?style=flat-square&logo=redis&logoColor=white" alt="Redis 7" />
30
29
  <img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License" />
31
30
  </p>
32
31
 
@@ -34,67 +33,78 @@
34
33
 
35
34
  ## What is atomic-queues?
36
35
 
37
- **A distributed virtual actor runtime for Node.js, built entirely on Redis primitives.**
36
+ **Per-entity sequential processing with virtual actors for NestJS.**
38
37
 
39
- Think Orleans or Akka but for the NestJS ecosystem, requiring nothing beyond a Redis instance you probably already have.
38
+ One worker per entity instance, spawned on demand, destroyed when idle. The worker IS the serialization boundary. If only one worker exists for `account:a-123` across the entire cluster, all operations on that account are serial by construction. No locks. No transactions. No race conditions.
40
39
 
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.
40
+ **Motto: Strictly once, fail if interrupted.**
42
41
 
43
42
  ```
44
43
  npm install atomic-queues ioredis
45
44
  ```
46
45
 
46
+ **Peer dependencies:** `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`, `ioredis`
47
+
48
+ **Optional:** `@grpc/grpc-js`, `@grpc/proto-loader` (cluster mode), `zod` (CLI schema validation)
49
+
47
50
  ---
48
51
 
49
52
  ## The Problem
50
53
 
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.
52
-
53
54
  ```
54
55
  Time Request A Request B Database
55
56
  ──────────────────────────────────────────────────────────────────────────
56
- T₀ SELECT balance $100 SELECT balance $100 $100
57
- T₁ CHECK: $100 $80CHECK: $100 $80
58
- T₂ UPDATE: $100 $80 = $20 $20
59
- T₃ UPDATE: $100 $80 = $20 −$60
57
+ T0 SELECT balance -> $100 SELECT balance -> $100 $100
58
+ T1 CHECK: $100 >= $80 CHECK: $100 >= $80
59
+ T2 UPDATE: $100 - $80 = $20 $20
60
+ T3 UPDATE: $100 - $80 = $20 -$60
60
61
  ──────────────────────────────────────────────────────────────────────────
61
- Result: Balance is −$60. Both withdrawals succeed. Integrity violated.
62
+ Result: Balance is -$60. Both withdrawals succeed. Integrity violated.
62
63
  ```
63
64
 
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
+ Row locks, optimistic locking, Redlock they all trade throughput for correctness.
65
66
 
66
67
  ## The Insight
67
68
 
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.
69
+ Don't lock the database. Don't lock the resource. **Route all operations for a given entity through a single worker.** That worker processes messages sequentially. Different entities have their own workers running concurrently.
71
70
 
72
71
  ```
73
- ┌─────────────────────────────────────────────────┐
74
- Request A ─┐ │ Entity: account-42 │
75
- │ │ ┌──────┐ ┌──────┐ ┌──────┐ │
76
- Request B ─┼─► Route ─┼─►│ Msg1 │─►│ Msg2 │─►│ Msg3 │─► [Executor] ─┐ │
77
- │ │ └──────┘ └──────┘ └──────┘ │ │
78
- Request C ─┘ │ Sequential ◄────────────┘ │
79
- └─────────────────────────────────────────────────┘
80
-
81
- Meanwhile, account-99, order-7, user-abc — all execute
82
- in parallel on the same cluster, completely independent.
72
+ account:a-1 ──► [Worker] ──► handler1 → handler2 → handler3 (sequential)
73
+ account:a-2 ──► [Worker] ──► handler1 → handler2 (sequential)
74
+ order:o-5 ──► [Worker] ──► handler1 (sequential)
75
+ (all concurrent across entities)
83
76
  ```
84
77
 
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.
78
+ One worker per entity. Spawned when a message arrives. Destroyed when idle. The worker runs on the event loop async I/O interleaves naturally across entities. No threads, no separate processes, no extra NestJS contexts.
86
79
 
87
80
  ---
88
81
 
89
- ## How It Works
82
+ ## Quick Start
90
83
 
91
- ### Entities and messages
84
+ ### 1. Register the module
85
+
86
+ ```typescript
87
+ @Module({
88
+ imports: [
89
+ AtomicQueuesModule.forRoot({
90
+ redis: { host: 'localhost', port: 6379 },
91
+ entities: {
92
+ account: {},
93
+ order: { onInterrupt: 'dead-letter' },
94
+ },
95
+ }),
96
+ ],
97
+ })
98
+ export class AppModule {}
99
+ ```
92
100
 
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:
101
+ ### 2. Define commands
94
102
 
95
103
  ```typescript
104
+ import { EntityType, QueueEntityId, Reply } from 'atomic-queues';
105
+
96
106
  @EntityType('account')
97
- export class WithdrawCommand {
107
+ class DepositCommand implements Reply<{ balance: number }> {
98
108
  constructor(
99
109
  @QueueEntityId() public readonly accountId: string,
100
110
  public readonly amount: number,
@@ -102,506 +112,375 @@ export class WithdrawCommand {
102
112
  }
103
113
  ```
104
114
 
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.
106
-
107
- ### Two levels of abstraction
108
-
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.
115
+ ### 3. Handle commands
110
116
 
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. The entity type is inferred from the message classes' `@EntityType` decoratorno `@Actor` decorator required.
117
+ Standard `@nestjs/cqrs` handlersnothing new to learn:
112
118
 
113
119
  ```typescript
114
- @Injectable()
115
- export class AccountActor {
116
- private balance = 0;
117
-
118
- @On(DepositCommand) // DepositCommand has @EntityType('account')
119
- async deposit(msg: DepositCommand) {
120
- this.balance += msg.amount;
121
- return this.balance;
122
- }
123
-
124
- @On(WithdrawCommand) // WithdrawCommand has @EntityType('account')
125
- async withdraw(msg: WithdrawCommand) {
126
- if (this.balance < msg.amount) throw new InsufficientFunds();
127
- this.balance -= msg.amount;
128
- return this.balance;
120
+ @CommandHandler(DepositCommand)
121
+ class DepositHandler implements ICommandHandler<DepositCommand> {
122
+ async execute(cmd: DepositCommand) {
123
+ // Runs sequentially per accountId — no concurrent deposits to the same account
124
+ const balance = await this.accountService.deposit(cmd.accountId, cmd.amount);
125
+ return { balance };
129
126
  }
130
127
  }
131
128
  ```
132
129
 
133
- **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.
130
+ ### 4. Dispatch
134
131
 
135
132
  ```typescript
136
- @EntityType('account')
137
- export class WithdrawCommand {
138
- constructor(
139
- @QueueEntityId() public readonly accountId: string,
140
- public readonly amount: number,
141
- ) {}
142
- }
133
+ import { QueueBus } from 'atomic-queues';
134
+
135
+ @Injectable()
136
+ class PaymentService {
137
+ constructor(private readonly queueBus: QueueBus) {}
143
138
 
144
- @CommandHandler(WithdrawCommand)
145
- export class WithdrawHandler implements ICommandHandler<WithdrawCommand> {
146
- async execute(cmd: WithdrawCommand) {
147
- // This runs sequentially per account — cluster-wide.
148
- // No locks. No transactions. The dispatch engine guarantees it.
139
+ async deposit(accountId: string, amount: number) {
140
+ // Fire and forget
141
+ await this.queueBus.enqueue(new DepositCommand(accountId, amount));
142
+
143
+ // Wait for typed result (Reply<R> branding)
144
+ const { balance } = await this.queueBus.enqueueAndWait(
145
+ new DepositCommand(accountId, amount),
146
+ );
149
147
  }
150
148
  }
151
149
  ```
152
150
 
153
- 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.
154
-
155
- ### Enqueuing messages
156
-
157
- ```typescript
158
- // Fire-and-forget
159
- await queueBus.enqueue(new WithdrawCommand(accountId, 100));
160
-
161
- // Enqueue and block until result — return type inferred from Reply<T> brand
162
- const balance = await queueBus.enqueueAndWait(new GetBalanceQuery(accountId));
163
-
164
- // Scoped to an entity type
165
- await queueBus.forEntity('account').enqueueBulk([charge1, charge2, charge3]);
166
-
167
- // Cross-service: string-based API — no class import needed
168
- await queueBus.enqueue('warehouse', 'ReserveStockCommand', 'SKU-001', { sku: 'SKU-001', quantity: 50 });
169
- const stock = await queueBus.enqueueAndWait('warehouse', 'GetStockQuery', 'SKU-001', { sku: 'SKU-001' });
170
-
171
- // Scoped cross-service
172
- const warehouse = queueBus.forEntity('warehouse');
173
- await warehouse.enqueue('ReserveStockCommand', 'SKU-001', { sku: 'SKU-001', quantity: 50 });
174
-
175
- // Actor-style direct send
176
- await actorSystem.send('account', accountId, new DepositCommand(100));
177
- const balance = await actorSystem.sendAndWait('account', accountId, new GetBalanceQuery());
178
- ```
151
+ First message for `account:a-123` spawns a worker. All subsequent messages for that account queue behind it. The handler runs on your app's event loop using your existing DI container.
179
152
 
180
153
  ---
181
154
 
182
- ## The Dispatch Engine
183
-
184
- 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.
185
-
186
- ### Per-entity message logs
187
-
188
- 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.
189
-
190
- 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.
191
-
192
- ### The dispatch gate
193
-
194
- 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.
195
-
196
- ### Atomic Lua scheduling
197
-
198
- A single Lua script runs atomically in Redis to perform the entire dispatch cycle:
199
-
200
- 1. Sample entities from the ready set (`SRANDMEMBER` with batch size 32)
201
- 2. Try to acquire the gate for each candidate (`SET NX EX`)
202
- 3. On first successful acquisition, pop the next message from that entity's log (`RPOP`)
203
- 4. Remove the entity from the ready set if its log is now empty
155
+ ## Queries
204
156
 
205
- 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.
157
+ Queries work identically to commands but route through the `QueryBus`. They are sequenced with commands a query enqueued after a deposit will see the deposit's effect.
206
158
 
207
- ### Shared executor pool
208
-
209
- 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.
210
-
211
- 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.
212
-
213
- ### Gate refresh for long-running handlers
214
-
215
- 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.
216
-
217
- ### Multiplexed result collection
159
+ ```typescript
160
+ @EntityType('account')
161
+ class GetBalanceQuery implements Reply<{ balance: number }> {
162
+ constructor(@QueueEntityId() public readonly accountId: string) {}
163
+ }
218
164
 
219
- 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.
165
+ const { balance } = await queueBus.enqueueAndWait(new GetBalanceQuery('acc-123'));
166
+ ```
220
167
 
221
168
  ---
222
169
 
223
- ## Cross-Service Communication
224
-
225
- This is where atomic-queues stops being a "queue library" and becomes a **distributed coordination primitive**.
226
-
227
- ### The problem it solves
228
-
229
- 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.
170
+ ## How It Works
230
171
 
231
- atomic-queues replaces all of that with Redis.
172
+ ### Virtual Actors (EntityWorker)
232
173
 
233
- ### How it works
174
+ Each entity instance (`account:a-123`, `order:o-5`) gets its own virtual actor — a processor callback with a FIFO message queue. The actor:
234
175
 
235
- 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.
176
+ 1. Spawns on first message (no pre-registration needed)
177
+ 2. Processes messages sequentially (one at a time, on the event loop)
178
+ 3. Yields at `await` points (other entities' actors proceed concurrently)
179
+ 4. Tears down after idle timeout (configurable, default 30s)
236
180
 
237
- ```typescript
238
- // warehouse-service: defines and handles the entity
239
- AtomicQueuesModule.forRoot({
240
- redis: { url: process.env.REDIS_URL },
241
- registry: { enabled: true, serviceName: 'warehouse-service' },
242
- })
181
+ ### Write-Ahead Log (WAL)
243
182
 
244
- // order-service: generate classes from the live registry, then use them like local CQRS
245
- import { ReserveStockCommand, GetStockQuery } from './generated';
183
+ Every message is dual-written: in-memory queue (speed) + Redis WAL (durability). The WAL is a state machine:
246
184
 
247
- await queueBus.enqueue(new ReserveStockCommand({ sku: 'SKU-001', quantity: 50 }));
248
- const stock = await queueBus.enqueueAndWait(new GetStockQuery({ sku: 'SKU-001' }));
249
- stock.available; // fully typed — no string API, no explicit timeout, no code dependency on warehouse-service
185
+ ```
186
+ enqueued dispatched completed | failed | interrupted
250
187
  ```
251
188
 
252
- When `warehouse-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, optional JSON schemas, and reply schemas, refreshed via heartbeat TTL. When `order-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.
253
-
254
- The Lua scheduler ensures each node only dispatches messages for entity types it owns handlers for. Services that don't own any handlers (API gateways, pure producers) participate in the registry without stealing messages from handler-owning nodes.
255
-
256
- ### What this replaces
257
-
258
- Think about what you no longer need:
259
-
260
- **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.
261
-
262
- **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.
263
-
264
- **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.
189
+ Each transition is an atomic Lua script that checks the current state before moving forward. Recovery runs automatically on startup:
265
190
 
266
- **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.
191
+ - `enqueued` re-dispatch (handler never ran this is the first attempt, not a retry)
192
+ - `dispatched` → **dead-letter** (handler was running when the process crashed — never re-execute)
193
+ - `completed` / `failed` / `interrupted` → cleanup (stale terminal entries)
267
194
 
268
- **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 decorated TypeScript classes so Service A gets compile-time type safety for messages destined to Service B, without importing Service B's code.)
195
+ A background cleanup timer evicts terminal WAL entries on a configurable interval.
269
196
 
270
- **No separate dead-letter infrastructure.** Failed messages are dead-lettered per entity type in Redis, queryable via the same connection.
197
+ ### Master Topology (Cluster Mode)
271
198
 
272
- ### Schema validation
199
+ Each replica set has a **deterministic master** — the node with the lowest `serverId` among live nodes in the same `serviceGroup`. No locks, no elections, no Redlock. All nodes read the same Redis-backed heartbeat registry and independently compute who the master is.
273
200
 
274
- Attach Zod schemas to message classes for runtime safety across service boundaries:
201
+ The master:
275
202
 
276
- ```typescript
277
- import { Schema } from 'atomic-queues';
278
- import { z } from 'zod';
203
+ - Owns the **worker assignment table**: which `entity:entityId` lives on which replica
204
+ - Routes all petitions: replicas forward via gRPC to the master
205
+ - Resolves workers via three tiers: existing assignment → consistent hash ring → least-loaded replica
206
+ - **Epoch fences** every dispatch: replicas reject commands from stale masters
279
207
 
280
- @Schema(z.object({
281
- accountId: z.string().uuid(),
282
- amount: z.number().positive(),
283
- }))
284
- @EntityType('account')
285
- export class WithdrawCommand {
286
- @QueueEntityId() public readonly accountId: string;
287
- public readonly amount: number;
288
- }
208
+ ```
209
+ Replica Set: billing-service
210
+ ┌──────────────────────────────────────────────┐
211
+ │ Master (deterministic: lowest serverId)
212
+ │ ├── Assignment Table │
213
+ │ │ account:a-1 replica-2 │
214
+ │ account:a-2 replica-1 │
215
+ └── Routes petitions, balances load │
216
+ │ │
217
+ │ Replica-1: [worker: account:a-2] │
218
+ │ Replica-2: [worker: account:a-1] │
219
+ │ Replica-3: (master pod, no workers yet) │
220
+ └──────────────────────────────────────────────┘
289
221
  ```
290
222
 
291
- 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.
223
+ Masters interconnect across service groups:
224
+ ```
225
+ Master (billing) ←── gRPC ──→ Master (warehouse)
226
+ ```
292
227
 
293
- ### Entity co-ownership
228
+ ### Master Failover
294
229
 
295
- 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.
230
+ 1. Master crashes heartbeat TTL expires
231
+ 2. Remaining nodes recompute leader from node list → next-lowest `serverId` becomes master
232
+ 3. New master queries all replicas via gRPC `ListWorkers`
233
+ 4. Rebuilds assignment table from live cluster state (petitions rejected during rebuild — fail-fast over misrouting)
234
+ 5. Old master pushes its worker list to the new master on demotion
235
+ 6. Resumes operations
296
236
 
297
- ### Runtime introspection
237
+ No split-brain: leadership is a pure function of the live node set. Epoch fencing rejects any stale-master commands that arrive during transitions.
298
238
 
299
- Any service can discover what the cluster offers at runtime — no config files, no shared code:
239
+ ### Health Monitoring
300
240
 
301
- ```typescript
302
- const contracts = await queueBus.introspect();
241
+ **Redis health**: Periodic `PING`. Consecutive failures above threshold → degraded mode (reject new messages, leader resigns, discovery steps down). Automatic recovery when Redis responds again.
303
242
 
304
- contracts.entityTypes(); // ['account', 'warehouse', ...]
305
- contracts.hasEntity('warehouse'); // true
306
- contracts.messagesFor('warehouse'); // ['ReserveStockCommand', 'GetStockQuery']
307
- contracts.accepts('warehouse', 'ReserveStockCommand'); // true
308
- contracts.schemaFor('warehouse', 'ReserveStockCommand'); // { properties: { sku: ..., quantity: ... } }
309
- contracts.replySchemaFor('warehouse', 'GetStockQuery'); // { properties: { sku: ..., available: ... } }
243
+ **gRPC peer connectivity**: Native gRPC channel state watching (`READY` alive, `TRANSIENT_FAILURE` → suspected dead). Debounce timer prevents flapping on brief disconnects.
310
244
 
311
- // Human-readable summary for logging/debugging
312
- console.log(contracts.toString());
313
- ```
245
+ **Per-peer circuit breakers**: gRPC connections track consecutive failures. After threshold → circuit opens (fast-fail, no network calls). After cooldown → half-open (one probe). Success → closed. Failure → re-open.
314
246
 
315
- ### Raw cross-service API
247
+ ---
316
248
 
317
- For quick prototyping or dynamic dispatch, you can also use the string-based API — no classes, no codegen, no imports:
249
+ ## Enqueuing Messages
318
250
 
319
251
  ```typescript
320
252
  // Fire-and-forget
321
- await queueBus.enqueue('warehouse', 'ReserveStockCommand', 'SKU-001', {
322
- sku: 'SKU-001',
323
- quantity: 50,
324
- });
325
-
326
- // Request-reply
327
- const stock = await queueBus.enqueueAndWait('warehouse', 'GetStockQuery', 'SKU-001', {
328
- sku: 'SKU-001',
329
- });
330
-
331
- // Scoped to an entity type
332
- const warehouse = queueBus.forEntity('warehouse');
333
- await warehouse.enqueue('ReserveStockCommand', 'SKU-001', { sku: 'SKU-001', quantity: 50 });
334
- const stock = await warehouse.enqueueAndWait('GetStockQuery', 'SKU-001', { sku: 'SKU-001' });
335
- ```
336
-
337
- This works out of the box — the registry validates entity type and message name at the call site. For production services, class codegen gives you full type safety.
338
-
339
- ### Class codegen (recommended)
340
-
341
- Generate fully decorated TypeScript classes from the live registry — import them and use them like local CQRS classes with full autocomplete, type safety, and zero string APIs:
342
-
343
- ```bash
344
- npx atomic-queues generate --classes -o src/generated
345
- ```
346
-
347
- This produces one file per entity type plus a barrel `index.ts`:
348
-
349
- ```
350
- src/generated/
351
- warehouse.ts # ReserveStockCommand, GetStockQuery, data interfaces, reply interfaces
352
- billing.ts # ChargeCommand, GetInvoiceQuery, ...
353
- index.ts # export * from './warehouse'; export * from './billing';
354
- ```
253
+ await queueBus.enqueue(new WithdrawCommand(accountId, 100));
355
254
 
356
- Then use them exactly like local command/query classes:
255
+ // Enqueue and wait for typed result
256
+ const { balance } = await queueBus.enqueueAndWait(new GetBalanceQuery(accountId));
357
257
 
358
- ```typescript
359
- import { ReserveStockCommand, GetStockQuery } from './generated';
258
+ // Scoped API
259
+ const account = queueBus.forEntity('account', accountId);
260
+ await account.enqueue(new DepositCommand(accountId, 500));
360
261
 
361
- // Fire-and-forget full autocomplete on constructor fields
362
- await queueBus.enqueue(new ReserveStockCommand({ sku: 'SKU-001', quantity: 50 }));
363
-
364
- // Request-reply — return type inferred from Reply<T> brand, no explicit timeout
365
- const stock = await queueBus.enqueueAndWait(new GetStockQuery({ sku: 'SKU-001' }));
366
- stock.available; // typed as number — full IDE support
262
+ // Raw string API (cross-service, no class needed)
263
+ await queueBus.enqueue('warehouse', 'ReserveStockCommand', 'SKU-001', {
264
+ sku: 'SKU-001', quantity: 50,
265
+ });
367
266
  ```
368
267
 
369
- Generated query classes implement `Reply<T>` via a phantom type brand, so `enqueueAndWait` infers the return type at compile time with zero runtime cost. No explicit generics, no timeout parameter — timeouts are resolved from config.
370
-
371
- You can also filter to specific entity types:
268
+ ---
372
269
 
373
- ```bash
374
- npx atomic-queues generate --classes -o src/generated --entities warehouse,billing
375
- ```
270
+ ## Backpressure
376
271
 
377
- ### Other codegen formats
272
+ Three levels, all configurable:
378
273
 
379
- ```bash
380
- # TypeScript interfaces + DispatchMap (for typed string-based API)
381
- npx atomic-queues generate --ts --output ./generated/contracts.ts
274
+ | Level | Config | Behavior |
275
+ |-------|--------|----------|
276
+ | Per-worker | `workerMaxQueueDepth` | Rejects with `QUEUE_DEPTH_EXCEEDED` |
277
+ | Global workers | `maxTotalWorkers` | Rejects new entities with `WORKER_LIMIT_EXCEEDED` (existing entities still accepted) |
278
+ | Global depth | `maxTotalQueueDepth` | Rejects all enqueues with `QUEUE_DEPTH_EXCEEDED` |
382
279
 
383
- # JSON Schema (language-agnostic)
384
- npx atomic-queues generate --json-schema --output ./generated/schema.json
280
+ In cluster mode, the master also enforces `maxConcurrentPetitions` to bound petition processing.
385
281
 
386
- # Full registry snapshot
387
- npx atomic-queues generate --snapshot --output ./generated/snapshot.json
388
- ```
282
+ ---
389
283
 
390
- ### Config-driven timeouts
284
+ ## Configuration
391
285
 
392
- `enqueueAndWait` resolves timeouts automatically — you never need to pass one explicitly:
286
+ ### Minimal (single server)
393
287
 
394
288
  ```typescript
395
289
  AtomicQueuesModule.forRoot({
396
- executor: {
397
- gateTTL: 30,
398
- defaultReplyTimeout: 15000, // global fallback: 15s
399
- },
400
- entities: {
401
- warehouse: {
402
- replyTimeout: 5000, // warehouse-specific: 5s
403
- },
404
- },
290
+ redis: { host: 'localhost', port: 6379 },
405
291
  })
406
292
  ```
407
293
 
408
- Resolution chain: explicit arg per-entity `replyTimeout` global `defaultReplyTimeout` `gateTTL * 2 * 1000`. If nothing is configured, defaults to 60s.
294
+ That's it. Everything else has defaults. Add `entities` to customize per-entity behavior, `grpc` to enable cluster mode.
409
295
 
410
- ---
296
+ ### Full reference
411
297
 
412
- ## Redis *is* the Protocol
298
+ #### `AtomicQueuesModule.forRoot(config)`
413
299
 
414
- This is the most important architectural decision in the project, and it has implications that go far beyond NestJS.
300
+ | Field | Type | Required | Default | Description |
301
+ |-------|------|----------|---------|-------------|
302
+ | `redis` | `IRedisConfig` | **yes** | — | Redis connection. Accepts `{ host, port, password, db }` or `{ url }` |
303
+ | `entities` | `Record<string, IEntityConfig>` | no | `{}` | Per-entity-type overrides (see below) |
304
+ | `keyPrefix` | `string` | no | `'aq'` | Prefix for all Redis keys |
305
+ | `maxTotalWorkers` | `number` | no | `10000` | Max concurrent entity workers across all types. `0` = unbounded |
306
+ | `maxTotalQueueDepth` | `number` | no | `100000` | Max total pending messages across all workers. `0` = unbounded |
307
+ | `retry` | `IRetryPolicy` | no | `{ maxAttempts: 1 }` | Default retry policy (strictly-once by default) |
308
+ | `wal` | `IWalConfig` | no | `{ enabled: true }` | Write-ahead log settings |
309
+ | `grpc` | `IGrpcConfig` | no | `{ enabled: false }` | Cluster mode — omit entirely for single-server |
310
+ | `verbose` | `boolean` | no | `false` | Enable verbose logging |
415
311
 
416
- The wire protocol is [fully documented](./WIRE-PROTOCOL.md), intentionally simple, and versioned with breaking-change semantics. Enqueuing a message is three Redis commands:
312
+ #### `IEntityConfig` per entity type
417
313
 
314
+ ```typescript
315
+ entities: {
316
+ account: { /* all fields optional */ },
317
+ order: { onInterrupt: 'dead-letter', workerIdleTimeout: 60_000 },
318
+ }
418
319
  ```
419
- LPUSH aq:log:account:a-1 '<message JSON>'
420
- SADD aq:ready account:a-1
421
- PUBLISH aq:tickle 1
422
- ```
423
-
424
- **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.
425
-
426
- 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.
427
320
 
428
- 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.
321
+ | Field | Type | Default | Description |
322
+ |-------|------|---------|-------------|
323
+ | `defaultEntityId` | `string` | — | Property name used as entity ID when `@QueueEntityId` is not present |
324
+ | `onInterrupt` | `'dead-letter' \| 'retry'` | `'dead-letter'` | What to do when a message is found mid-execution on recovery |
325
+ | `workerIdleTimeout` | `number` (ms) | `30000` | How long an idle worker lives before teardown |
326
+ | `workerMaxQueueDepth` | `number` | `0` (unbounded) | Max pending messages per worker. Rejects with `QUEUE_DEPTH_EXCEEDED` |
327
+ | `replyTimeout` | `number` (ms) | `5000` | Default timeout for `enqueueAndWait` on this entity type |
328
+ | `retry` | `IRetryPolicy` | inherits root | Per-entity retry policy override |
429
329
 
430
- This opens architectures that are genuinely difficult to build otherwise:
330
+ #### `IRetryPolicy`
431
331
 
432
- - **Ingest in Go, process in Node.js, analyze in Python.** Each layer speaks Redis. The entity logs are the integration boundary.
433
- - **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.
434
- - **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.
435
- - **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.
332
+ | Field | Type | Default | Description |
333
+ |-------|------|---------|-------------|
334
+ | `maxAttempts` | `number` | `1` | Total attempts. `1` = strictly once, no retries |
335
+ | `backoff` | `'fixed' \| 'exponential'` | `'exponential'` | Backoff strategy between retries |
336
+ | `backoffDelay` | `number` (ms) | `1000` | Base delay between retries |
337
+ | `maxDelay` | `number` (ms) | `30000` | Maximum delay cap for exponential backoff |
436
338
 
437
- ---
339
+ #### `IWalConfig` — write-ahead log
438
340
 
439
- ## Quick Start
341
+ | Field | Type | Default | Description |
342
+ |-------|------|---------|-------------|
343
+ | `enabled` | `boolean` | `true` | Disable WAL for testing only — **never disable in production** |
344
+ | `cleanupInterval` | `number` (ms) | `5000` | How often to evict completed/failed WAL entries |
345
+ | `entryTTL` | `number` (seconds) | `86400` (24h) | Safety TTL for WAL entries in Redis |
440
346
 
441
- ```typescript
442
- import { Module } from '@nestjs/common';
443
- import { AtomicQueuesModule } from 'atomic-queues';
444
-
445
- @Module({
446
- imports: [
447
- AtomicQueuesModule.forRoot({
448
- redis: { host: 'localhost', port: 6379 },
449
- }),
450
- ],
451
- })
452
- export class AppModule {}
453
- ```
347
+ #### `IGrpcConfig` — cluster mode
454
348
 
455
- Define a command and enqueue it:
349
+ Omit entirely for single-server. Set `enabled: true` to activate.
456
350
 
457
351
  ```typescript
458
- @EntityType('account')
459
- export class WithdrawCommand {
460
- constructor(
461
- @QueueEntityId() public readonly accountId: string,
462
- public readonly amount: number,
463
- ) {}
464
- }
465
-
466
- @Injectable()
467
- export class PaymentService {
468
- constructor(private readonly queueBus: QueueBus) {}
469
-
470
- async withdraw(accountId: string, amount: number) {
471
- await this.queueBus.enqueue(new WithdrawCommand(accountId, amount));
472
- }
352
+ grpc: {
353
+ enabled: true,
354
+ listenAddress: '0.0.0.0:50051',
355
+ advertisedAddress: '10.0.1.5:50051',
356
+ serverId: 'billing-1',
357
+ serviceGroup: 'billing',
473
358
  }
474
359
  ```
475
360
 
476
- 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.
361
+ | Field | Type | Default | Description |
362
+ |-------|------|---------|-------------|
363
+ | `enabled` | `boolean` | `false` | Enable gRPC cluster transport |
364
+ | `listenAddress` | `string` | `'0.0.0.0:50051'` | Address the gRPC server binds to |
365
+ | `advertisedAddress` | `string` | `os.hostname() + ':50051'` | Address other nodes use to reach this one |
366
+ | `serverId` | `string` | auto-generated UUID | Unique node ID. Must be stable across restarts for predictable leader election |
367
+ | `serviceGroup` | `string` | `'default'` | Logical grouping — nodes in the same group form a replica set |
368
+ | `maxForwardHops` | `number` | `3` | Max cross-service forwarding hops to prevent loops |
369
+ | `maxConcurrentPetitions` | `number` | `50` | Max in-flight petitions the master processes. `0` = unbounded |
370
+
371
+ **Timing (ms)**
372
+
373
+ | Field | Default | Description |
374
+ |-------|---------|-------------|
375
+ | `heartbeatMs` | `400` | How often this node heartbeats to Redis |
376
+ | `nodeTTLMs` | `1500` | Node considered dead after this long without heartbeat |
377
+ | `reconcileIntervalMs` | `2000` | How often to scan Redis for membership changes |
378
+ | `leaderTTLMs` | `2000` | Leader lock TTL |
379
+ | `leaderRenewalMs` | `400` | Leader lock renewal interval |
380
+ | `leaderDebounceMs` | `800` | Debounce window before recomputing leader after ring changes |
381
+
382
+ **Health monitoring**
383
+
384
+ | Field | Default | Description |
385
+ |-------|---------|-------------|
386
+ | `peerMonitorEnabled` | `true` | Watch gRPC channel state for fast failure detection |
387
+ | `peerSuspectDebounceMs` | `500` | Debounce before declaring a peer suspected-dead |
388
+ | `redisHealthCheckMs` | `500` | Redis PING interval |
389
+ | `redisHealthFailureThreshold` | `3` | Consecutive PING failures before degraded mode |
390
+
391
+ **Circuit breaker (per-peer gRPC connections)**
392
+
393
+ | Field | Default | Description |
394
+ |-------|---------|-------------|
395
+ | `circuitBreakerFailureThreshold` | `3` | Consecutive failures before opening the circuit |
396
+ | `circuitBreakerCooldownMs` | `2000` | Time before a half-open probe is allowed |
397
+
398
+ **gRPC keepalive**
399
+
400
+ | Field | Default | Description |
401
+ |-------|---------|-------------|
402
+ | `keepaliveTimeMs` | `10000` | Keepalive ping interval (minimum enforced by grpc-js) |
403
+ | `keepaliveTimeoutMs` | `5000` | Connection dead if no keepalive response |
404
+
405
+ **RPC deadlines** (`deadlines` sub-object)
406
+
407
+ | Field | Default | Description |
408
+ |-------|---------|-------------|
409
+ | `deadlines.forwardMs` | `1500` | Deadline for fire-and-forget RPCs (forward, petition, enqueueToWorker) |
410
+ | `deadlines.pingMs` | `1000` | Deadline for health ping |
411
+ | `deadlines.andWaitMs` | `60000` | Default deadline for `*AndWait` RPCs when no `replyTimeout` is set |
412
+ | `deadlines.syncMs` | `1000` | Deadline for `listWorkers` during master table rebuild |
413
+ | `deadlines.connectivityWatchMs` | `30000` | Timeout for peer connectivity watch loop re-arm |
477
414
 
478
415
  ---
479
416
 
480
- ## Configuration
481
-
482
- ```typescript
483
- AtomicQueuesModule.forRoot({
484
- redis: { host: 'localhost', port: 6379 },
485
-
486
- executor: {
487
- poolSize: 1, // concurrent executors per node
488
- gateTTL: 30, // seconds before gate expires (safety net)
489
- defaultReplyTimeout: 15000, // global default for enqueueAndWait (ms)
490
- },
491
-
492
- entities: {
493
- account: {
494
- defaultEntityId: 'accountId',
495
- gateTTL: 60,
496
- retry: { maxAttempts: 5, backoff: 'exponential', backoffDelay: 2000 },
497
- actorIdleTimeout: 120000,
498
- statePersistence: true,
499
- replyTimeout: 5000, // per-entity enqueueAndWait timeout (ms)
500
- },
501
- },
502
-
503
- registry: {
504
- enabled: false,
505
- serviceName: 'my-service',
506
- schemaValidation: false,
507
- heartbeatInterval: 10000,
508
- registrationTTL: 30,
509
- },
510
-
511
- keyPrefix: 'aq',
512
- verbose: false,
513
- })
514
- ```
417
+ ## Dead Letter Queue
515
418
 
516
- Optional peer dependencies:
419
+ Messages found in `dispatched` state on recovery, or that exhaust all retry attempts, are moved to a Redis-backed dead letter queue.
517
420
 
518
421
  ```bash
519
- npm install @nestjs/cqrs # for CQRS handler auto-wiring
520
- npm install zod zod-to-json-schema # for schema validation in the registry
422
+ npx atomic-queues dlq list
423
+ npx atomic-queues dlq replay --id <message-id>
424
+ npx atomic-queues dlq purge
521
425
  ```
522
426
 
523
427
  ---
524
428
 
525
- ## Guarantees
526
-
527
- | Guarantee | Scope | Mechanism |
528
- |---|---|---|
529
- | FIFO per entity | Cluster-wide | Redis list (`LPUSH`/`RPOP`) |
530
- | Single-writer per entity | Cluster-wide | Gate key (`SET NX EX`) |
531
- | At-least-once delivery | Per message | Retry on gate TTL expiry |
532
- | Parallel across entities | Per node | Executor pool concurrency |
533
- | Durability | Per message | Redis persistence (AOF/RDB) |
534
-
535
- ### What this does NOT guarantee
429
+ ## CLI
536
430
 
537
- **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.
538
-
539
- ---
540
-
541
- ## How It Compares
431
+ ```bash
432
+ # Inspect live entity/command/query registry from Redis
433
+ npx atomic-queues introspect
542
434
 
543
- | Capability | BullMQ | Temporal | atomic-queues |
544
- |---|---|---|---|
545
- | Per-entity ordering | Manual (named queues) | Workflow-scoped | Built-in, zero config |
546
- | Cross-entity parallelism | Worker pools | Worker pools | Shared executor pool |
547
- | Stateful entities | No | Workflow state | Virtual actors |
548
- | Cross-service messaging | Shared queue names | gRPC | Redis registry + codegen |
549
- | Polyglot clients | JS/TS only | SDK per language | Any Redis client (3 commands) |
550
- | Infrastructure required | Redis | Temporal server + DB | Redis only |
551
- | Distributed locks needed | Yes, for ordering | Internal | None — gates are non-contending |
552
- | Service discovery | External | Built-in | Built-in (registry) |
553
- | Schema validation | No | Protobuf | Zod → JSON Schema |
435
+ # Generate TypeScript from the live registry
436
+ npx atomic-queues generate --classes -o ./src/generated # decorated class files
437
+ npx atomic-queues generate --ts -o ./src/generated # namespace interfaces + DispatchMap
438
+ npx atomic-queues generate --json-schema -o ./src/generated
439
+ ```
554
440
 
555
441
  ---
556
442
 
557
- ## Decorator Reference
443
+ ## Guarantees
558
444
 
559
- | Decorator | Purpose |
445
+ | Guarantee | Mechanism |
560
446
  |---|---|
561
- | `@EntityType('type')` | Route a message to an entity type |
562
- | `@QueueEntityId()` | Mark the property holding the entity ID |
563
- | `@QueueEntity('type', 'prop')` | Combined entity type + ID |
564
- | `@Actor('type')` | Explicitly declare an actor (optional entity type is inferred from `@On` handlers) |
565
- | `@On(MessageClass)` | Handle a message type on an actor |
566
- | `@Schema(zodSchema)` | Attach a Zod schema for registry validation |
567
- | `@ReplySchema(zodSchema)` | Attach a reply schema for query codegen |
447
+ | FIFO per entity | One worker per entity:entityId with FIFO queue |
448
+ | Single-writer per entity | Only one worker exists across the cluster |
449
+ | At-most-once delivery | WAL: enqueued dispatched completed. Never re-executed after dispatch. |
450
+ | Fail if interrupted | Dispatched on crash dead-lettered, source notified |
451
+ | Concurrent across entities | Event loop interleaves at await points |
452
+ | Durability | Redis WAL (dual-write: in-memory + Redis) |
453
+ | Auto-recovery | WAL recovery + cleanup run automatically on startup |
454
+ | Cluster coordination | Deterministic master topology with gRPC |
455
+ | Master failover | Heartbeat expiry → deterministic re-election + assignment table rebuild |
456
+ | Epoch fencing | Replicas reject commands from stale masters |
457
+ | No distributed locks | The worker IS the serialization — not a lock, not Redlock, not SET NX |
568
458
 
569
459
  ---
570
460
 
571
- ## Production Considerations
572
-
573
- ### Redis as a Single Point of Failure
461
+ ## Design Philosophy
574
462
 
575
- atomic-queues relies on a single Redis instance for all coordination: message logs, gates, the ready set, actor state, and the distributed registry. If that Redis instance becomes unavailable, all dispatch stops.
463
+ AtomicQueues is pessimistic by design. At every decision point, it chooses safety over liveness:
576
464
 
577
- **Mitigations:**
465
+ - **Interrupted?** Dead-letter, don't retry.
466
+ - **Redis down?** Reject new work, don't buffer.
467
+ - **Stale epoch?** Reject, don't process.
468
+ - **Master rebuilding?** Reject petitions, don't guess.
469
+ - **Unknown assignment?** Bounce and retry through the master, don't deliver speculatively.
578
470
 
579
- - **Redis Sentinel** — automatic failover to a replica. Gates (SET NX EX) and Lua scripts work identically after promotion. Brief message re-delivery is possible during failover but per-entity ordering is preserved.
580
- - **Redis Cluster** — horizontal scaling. Requires all keys for a given entity to land on the same shard. Use Redis hash tags (e.g. `{account:a-1}`) in your `keyPrefix` config to ensure co-location.
581
- - **Persistence** — enable AOF (`appendonly yes`) with `appendfsync everysec` at minimum. RDB snapshots alone risk losing the last seconds of enqueued messages on crash.
582
- - **Monitoring** — watch `connected_clients`, `used_memory`, and `instantaneous_ops_per_sec`. Set alerts on replication lag if using Sentinel.
583
-
584
- ### Retry Ordering
585
-
586
- Failed messages are re-enqueued with `RPUSH`, placing them at the back of the entity's log. This means other pending messages for the same entity are processed before the retry. If you need head-of-line retry (failed message retried immediately), implement a custom retry strategy.
587
-
588
- ### Actor State
589
-
590
- Actor state is serialized to Redis as JSON after each message. `Map`, `Set`, `Date`, and circular references are silently skipped during serialization. Keep actor state plain and serializable. State TTL defaults to 86400 seconds (24 hours) and is configurable per entity type via `stateTTL` in the module config.
471
+ The system refuses to operate under uncertainty rather than risk executing a message twice.
591
472
 
592
473
  ---
593
474
 
594
- ## Migrating from V1
595
-
596
- V2 is a full rewrite of the internals. BullMQ is removed. Workers are removed. The public API is largely preserved.
475
+ ## Migrating from v2
597
476
 
598
- **What stays the same**: `@EntityType`, `@QueueEntityId`, `@QueueEntity`, `queueBus.enqueue()`, `queueBus.forEntity()`, `queueBus.enqueueAndWait()`.
477
+ **Removed**: `executor`, `registry`, `gateTTL`, `ActorSystem`, `LogService`, `GateService`, `SchedulerService`, `ExecutorPoolService`, `ResultCollector`, `RegistryService`, `workers` config, `WorkerModule`.
599
478
 
600
- **What's removed**: `@WorkerProcessor`, `@JobHandler`, `@EntityScaler`, `@OnSpawnWorker`, `@OnTerminateWorker`, `@GetActiveEntities`, `@GetDesiredWorkerCount`, `.forProcessor()`. All worker and scaling concepts are gone.
479
+ **Added**: `EntityWorker`, `EntityWorkerManager`, `MasterCoordinator`, `workerIdleTimeout` in entity config.
601
480
 
602
- **What's new**: `@Actor`, `@On`, `@Schema`, `@ReplySchema`, `ActorSystem`, `RegistryService`, distributed registry, runtime introspection (`queueBus.introspect()`), cross-service string-based API, `Reply<T>` phantom type, class codegen CLI (`--classes`), config-driven timeouts.
481
+ **Unchanged**: All decorators, `QueueBus` public API, CLI generators.
603
482
 
604
- **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.
483
+ **Migration**: Remove `executor`/`registry`/`workers` from config. That's it. Workers are now internal.
605
484
 
606
485
  ---
607
486