@ddd-ts-kit/di 0.0.0-stage → 0.1.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 (446) hide show
  1. package/README.md +1075 -2
  2. package/dist/application/application-options.d.ts +19 -0
  3. package/dist/application/application-options.d.ts.map +1 -0
  4. package/dist/application/application-options.js +2 -0
  5. package/dist/application/application-options.js.map +1 -0
  6. package/dist/application/application.d.ts +22 -0
  7. package/dist/application/application.d.ts.map +1 -0
  8. package/dist/application/application.js +2 -0
  9. package/dist/application/application.js.map +1 -0
  10. package/dist/application/assemble.d.ts +6 -0
  11. package/dist/application/assemble.d.ts.map +1 -0
  12. package/dist/application/assemble.js +16 -0
  13. package/dist/application/assemble.js.map +1 -0
  14. package/dist/application/binding-info.d.ts +10 -0
  15. package/dist/application/binding-info.d.ts.map +1 -0
  16. package/dist/application/binding-info.js +2 -0
  17. package/dist/application/binding-info.js.map +1 -0
  18. package/dist/application/bound-ports.d.ts +8 -0
  19. package/dist/application/bound-ports.d.ts.map +1 -0
  20. package/dist/application/bound-ports.js +2 -0
  21. package/dist/application/bound-ports.js.map +1 -0
  22. package/dist/application/consumer-subscriptions.d.ts +21 -0
  23. package/dist/application/consumer-subscriptions.d.ts.map +1 -0
  24. package/dist/application/consumer-subscriptions.js +59 -0
  25. package/dist/application/consumer-subscriptions.js.map +1 -0
  26. package/dist/application/container-application.d.ts +44 -0
  27. package/dist/application/container-application.d.ts.map +1 -0
  28. package/dist/application/container-application.js +224 -0
  29. package/dist/application/container-application.js.map +1 -0
  30. package/dist/application/create-application.d.ts +10 -0
  31. package/dist/application/create-application.d.ts.map +1 -0
  32. package/dist/application/create-application.js +12 -0
  33. package/dist/application/create-application.js.map +1 -0
  34. package/dist/application/decorate.d.ts +8 -0
  35. package/dist/application/decorate.d.ts.map +1 -0
  36. package/dist/application/decorate.js +2 -0
  37. package/dist/application/decorate.js.map +1 -0
  38. package/dist/application/delegating-applications.d.ts +7 -0
  39. package/dist/application/delegating-applications.d.ts.map +1 -0
  40. package/dist/application/delegating-applications.js +6 -0
  41. package/dist/application/delegating-applications.js.map +1 -0
  42. package/dist/application/failures-of.d.ts +3 -0
  43. package/dist/application/failures-of.d.ts.map +1 -0
  44. package/dist/application/failures-of.js +13 -0
  45. package/dist/application/failures-of.js.map +1 -0
  46. package/dist/application/is-handler-registry.d.ts +9 -0
  47. package/dist/application/is-handler-registry.d.ts.map +1 -0
  48. package/dist/application/is-handler-registry.js +5 -0
  49. package/dist/application/is-handler-registry.js.map +1 -0
  50. package/dist/application/job-lifecycle.d.ts +16 -0
  51. package/dist/application/job-lifecycle.d.ts.map +1 -0
  52. package/dist/application/job-lifecycle.js +48 -0
  53. package/dist/application/job-lifecycle.js.map +1 -0
  54. package/dist/application/override-for-tests.d.ts +8 -0
  55. package/dist/application/override-for-tests.d.ts.map +1 -0
  56. package/dist/application/override-for-tests.js +14 -0
  57. package/dist/application/override-for-tests.js.map +1 -0
  58. package/dist/application/override.d.ts +5 -0
  59. package/dist/application/override.d.ts.map +1 -0
  60. package/dist/application/override.js +5 -0
  61. package/dist/application/override.js.map +1 -0
  62. package/dist/application/with-binding-aspects.d.ts +8 -0
  63. package/dist/application/with-binding-aspects.d.ts.map +1 -0
  64. package/dist/application/with-binding-aspects.js +26 -0
  65. package/dist/application/with-binding-aspects.js.map +1 -0
  66. package/dist/authorization/assert-authorization-declared.d.ts +5 -0
  67. package/dist/authorization/assert-authorization-declared.d.ts.map +1 -0
  68. package/dist/authorization/assert-authorization-declared.js +28 -0
  69. package/dist/authorization/assert-authorization-declared.js.map +1 -0
  70. package/dist/authorization/authorization-mode.d.ts +3 -0
  71. package/dist/authorization/authorization-mode.d.ts.map +1 -0
  72. package/dist/authorization/authorization-mode.js +2 -0
  73. package/dist/authorization/authorization-mode.js.map +1 -0
  74. package/dist/authorization/declarations-of.d.ts +4 -0
  75. package/dist/authorization/declarations-of.d.ts.map +1 -0
  76. package/dist/authorization/declarations-of.js +4 -0
  77. package/dist/authorization/declarations-of.js.map +1 -0
  78. package/dist/authorization/declared-policies.d.ts +10 -0
  79. package/dist/authorization/declared-policies.d.ts.map +1 -0
  80. package/dist/authorization/declared-policies.js +8 -0
  81. package/dist/authorization/declared-policies.js.map +1 -0
  82. package/dist/authorization/declared-requirement.d.ts +5 -0
  83. package/dist/authorization/declared-requirement.d.ts.map +1 -0
  84. package/dist/authorization/declared-requirement.js +16 -0
  85. package/dist/authorization/declared-requirement.js.map +1 -0
  86. package/dist/authorization/pass-every-message.d.ts +3 -0
  87. package/dist/authorization/pass-every-message.d.ts.map +1 -0
  88. package/dist/authorization/pass-every-message.js +2 -0
  89. package/dist/authorization/pass-every-message.js.map +1 -0
  90. package/dist/authorization/protected-requirement.d.ts +6 -0
  91. package/dist/authorization/protected-requirement.d.ts.map +1 -0
  92. package/dist/authorization/protected-requirement.js +9 -0
  93. package/dist/authorization/protected-requirement.js.map +1 -0
  94. package/dist/authorization/requirement-options.d.ts +8 -0
  95. package/dist/authorization/requirement-options.d.ts.map +1 -0
  96. package/dist/authorization/requirement-options.js +2 -0
  97. package/dist/authorization/requirement-options.js.map +1 -0
  98. package/dist/authorization/requirement.d.ts +5 -0
  99. package/dist/authorization/requirement.d.ts.map +1 -0
  100. package/dist/authorization/requirement.js +2 -0
  101. package/dist/authorization/requirement.js.map +1 -0
  102. package/dist/authorization/use-case-access.d.ts +2 -0
  103. package/dist/authorization/use-case-access.d.ts.map +1 -0
  104. package/dist/authorization/use-case-access.js +2 -0
  105. package/dist/authorization/use-case-access.js.map +1 -0
  106. package/dist/authorization/use-case-declaration.d.ts +14 -0
  107. package/dist/authorization/use-case-declaration.d.ts.map +1 -0
  108. package/dist/authorization/use-case-declaration.js +2 -0
  109. package/dist/authorization/use-case-declaration.js.map +1 -0
  110. package/dist/authorization/use-case-declarations.d.ts +7 -0
  111. package/dist/authorization/use-case-declarations.d.ts.map +1 -0
  112. package/dist/authorization/use-case-declarations.js +5 -0
  113. package/dist/authorization/use-case-declarations.js.map +1 -0
  114. package/dist/authorization/use-case-guard-middleware.d.ts +5 -0
  115. package/dist/authorization/use-case-guard-middleware.d.ts.map +1 -0
  116. package/dist/authorization/use-case-guard-middleware.js +6 -0
  117. package/dist/authorization/use-case-guard-middleware.js.map +1 -0
  118. package/dist/authorization/use-case-guard.d.ts +10 -0
  119. package/dist/authorization/use-case-guard.d.ts.map +1 -0
  120. package/dist/authorization/use-case-guard.js +8 -0
  121. package/dist/authorization/use-case-guard.js.map +1 -0
  122. package/dist/checks/assert-sound.d.ts +5 -0
  123. package/dist/checks/assert-sound.d.ts.map +1 -0
  124. package/dist/checks/assert-sound.js +74 -0
  125. package/dist/checks/assert-sound.js.map +1 -0
  126. package/dist/checks/cycle-error.d.ts +4 -0
  127. package/dist/checks/cycle-error.d.ts.map +1 -0
  128. package/dist/checks/cycle-error.js +6 -0
  129. package/dist/checks/cycle-error.js.map +1 -0
  130. package/dist/consumer/consumer-options.d.ts +14 -0
  131. package/dist/consumer/consumer-options.d.ts.map +1 -0
  132. package/dist/consumer/consumer-options.js +2 -0
  133. package/dist/consumer/consumer-options.js.map +1 -0
  134. package/dist/consumer/consumer-runtime.d.ts +9 -0
  135. package/dist/consumer/consumer-runtime.d.ts.map +1 -0
  136. package/dist/consumer/consumer-runtime.js +2 -0
  137. package/dist/consumer/consumer-runtime.js.map +1 -0
  138. package/dist/consumer/observed-projection.d.ts +19 -0
  139. package/dist/consumer/observed-projection.d.ts.map +1 -0
  140. package/dist/consumer/observed-projection.js +44 -0
  141. package/dist/consumer/observed-projection.js.map +1 -0
  142. package/dist/consumer/receive.d.ts +21 -0
  143. package/dist/consumer/receive.d.ts.map +1 -0
  144. package/dist/consumer/receive.js +40 -0
  145. package/dist/consumer/receive.js.map +1 -0
  146. package/dist/consumer/unreadable-event-error.d.ts +9 -0
  147. package/dist/consumer/unreadable-event-error.d.ts.map +1 -0
  148. package/dist/consumer/unreadable-event-error.js +15 -0
  149. package/dist/consumer/unreadable-event-error.js.map +1 -0
  150. package/dist/declared/apply-through.d.ts +3 -0
  151. package/dist/declared/apply-through.d.ts.map +1 -0
  152. package/dist/declared/apply-through.js +11 -0
  153. package/dist/declared/apply-through.js.map +1 -0
  154. package/dist/declared/consumption-of.d.ts +15 -0
  155. package/dist/declared/consumption-of.d.ts.map +1 -0
  156. package/dist/declared/consumption-of.js +15 -0
  157. package/dist/declared/consumption-of.js.map +1 -0
  158. package/dist/declared/context-module.d.ts +10 -0
  159. package/dist/declared/context-module.d.ts.map +1 -0
  160. package/dist/declared/context-module.js +16 -0
  161. package/dist/declared/context-module.js.map +1 -0
  162. package/dist/declared/declared-source.d.ts +7 -0
  163. package/dist/declared/declared-source.d.ts.map +1 -0
  164. package/dist/declared/declared-source.js +2 -0
  165. package/dist/declared/declared-source.js.map +1 -0
  166. package/dist/declared/discovered-files.d.ts +6 -0
  167. package/dist/declared/discovered-files.d.ts.map +1 -0
  168. package/dist/declared/discovered-files.js +10 -0
  169. package/dist/declared/discovered-files.js.map +1 -0
  170. package/dist/declared/include-declared.d.ts +10 -0
  171. package/dist/declared/include-declared.d.ts.map +1 -0
  172. package/dist/declared/include-declared.js +173 -0
  173. package/dist/declared/include-declared.js.map +1 -0
  174. package/dist/declared/projection-of.d.ts +24 -0
  175. package/dist/declared/projection-of.d.ts.map +1 -0
  176. package/dist/declared/projection-of.js +73 -0
  177. package/dist/declared/projection-of.js.map +1 -0
  178. package/dist/description/describe-wiring.d.ts +6 -0
  179. package/dist/description/describe-wiring.d.ts.map +1 -0
  180. package/dist/description/describe-wiring.js +77 -0
  181. package/dist/description/describe-wiring.js.map +1 -0
  182. package/dist/description/wiring-description.d.ts +98 -0
  183. package/dist/description/wiring-description.d.ts.map +1 -0
  184. package/dist/description/wiring-description.js +2 -0
  185. package/dist/description/wiring-description.js.map +1 -0
  186. package/dist/discovery/discover-context.d.ts +27 -0
  187. package/dist/discovery/discover-context.d.ts.map +1 -0
  188. package/dist/discovery/discover-context.js +69 -0
  189. package/dist/discovery/discover-context.js.map +1 -0
  190. package/dist/discovery/role-files-of.d.ts +16 -0
  191. package/dist/discovery/role-files-of.d.ts.map +1 -0
  192. package/dist/discovery/role-files-of.js +70 -0
  193. package/dist/discovery/role-files-of.js.map +1 -0
  194. package/dist/discovery/role-rule.d.ts +10 -0
  195. package/dist/discovery/role-rule.d.ts.map +1 -0
  196. package/dist/discovery/role-rule.js +23 -0
  197. package/dist/discovery/role-rule.js.map +1 -0
  198. package/dist/discovery.d.ts +2 -0
  199. package/dist/discovery.d.ts.map +1 -0
  200. package/dist/discovery.js +2 -0
  201. package/dist/discovery.js.map +1 -0
  202. package/dist/errors/wiring-error.d.ts +7 -0
  203. package/dist/errors/wiring-error.d.ts.map +1 -0
  204. package/dist/errors/wiring-error.js +10 -0
  205. package/dist/errors/wiring-error.js.map +1 -0
  206. package/dist/index.d.ts +52 -0
  207. package/dist/index.d.ts.map +1 -0
  208. package/dist/index.js +20 -0
  209. package/dist/index.js.map +1 -0
  210. package/dist/jobs/definition-of.d.ts +7 -0
  211. package/dist/jobs/definition-of.d.ts.map +1 -0
  212. package/dist/jobs/definition-of.js +11 -0
  213. package/dist/jobs/definition-of.js.map +1 -0
  214. package/dist/jobs/effective-job.d.ts +7 -0
  215. package/dist/jobs/effective-job.d.ts.map +1 -0
  216. package/dist/jobs/effective-job.js +2 -0
  217. package/dist/jobs/effective-job.js.map +1 -0
  218. package/dist/jobs/effective-jobs.d.ts +6 -0
  219. package/dist/jobs/effective-jobs.d.ts.map +1 -0
  220. package/dist/jobs/effective-jobs.js +33 -0
  221. package/dist/jobs/effective-jobs.js.map +1 -0
  222. package/dist/jobs/jobs-options.d.ts +16 -0
  223. package/dist/jobs/jobs-options.d.ts.map +1 -0
  224. package/dist/jobs/jobs-options.js +2 -0
  225. package/dist/jobs/jobs-options.js.map +1 -0
  226. package/dist/jobs/readable.d.ts +3 -0
  227. package/dist/jobs/readable.d.ts.map +1 -0
  228. package/dist/jobs/readable.js +15 -0
  229. package/dist/jobs/readable.js.map +1 -0
  230. package/dist/jobs/written-job.d.ts +11 -0
  231. package/dist/jobs/written-job.d.ts.map +1 -0
  232. package/dist/jobs/written-job.js +8 -0
  233. package/dist/jobs/written-job.js.map +1 -0
  234. package/dist/kernel/guarded-query-bus.d.ts +5 -0
  235. package/dist/kernel/guarded-query-bus.d.ts.map +1 -0
  236. package/dist/kernel/guarded-query-bus.js +9 -0
  237. package/dist/kernel/guarded-query-bus.js.map +1 -0
  238. package/dist/kernel/instrumentation.d.ts +37 -0
  239. package/dist/kernel/instrumentation.d.ts.map +1 -0
  240. package/dist/kernel/instrumentation.js +9 -0
  241. package/dist/kernel/instrumentation.js.map +1 -0
  242. package/dist/kernel/kernel-module-options.d.ts +57 -0
  243. package/dist/kernel/kernel-module-options.d.ts.map +1 -0
  244. package/dist/kernel/kernel-module-options.js +2 -0
  245. package/dist/kernel/kernel-module-options.js.map +1 -0
  246. package/dist/kernel/kernel-module.d.ts +14 -0
  247. package/dist/kernel/kernel-module.d.ts.map +1 -0
  248. package/dist/kernel/kernel-module.js +133 -0
  249. package/dist/kernel/kernel-module.js.map +1 -0
  250. package/dist/kernel/kernel-ports.d.ts +4 -0
  251. package/dist/kernel/kernel-ports.d.ts.map +1 -0
  252. package/dist/kernel/kernel-ports.js +25 -0
  253. package/dist/kernel/kernel-ports.js.map +1 -0
  254. package/dist/kernel/lazy-error-reporter.d.ts +8 -0
  255. package/dist/kernel/lazy-error-reporter.d.ts.map +1 -0
  256. package/dist/kernel/lazy-error-reporter.js +14 -0
  257. package/dist/kernel/lazy-error-reporter.js.map +1 -0
  258. package/dist/kernel/provided.d.ts +4 -0
  259. package/dist/kernel/provided.d.ts.map +1 -0
  260. package/dist/kernel/provided.js +2 -0
  261. package/dist/kernel/provided.js.map +1 -0
  262. package/dist/kernel/reported-failures.d.ts +5 -0
  263. package/dist/kernel/reported-failures.d.ts.map +1 -0
  264. package/dist/kernel/reported-failures.js +9 -0
  265. package/dist/kernel/reported-failures.js.map +1 -0
  266. package/dist/kernel/transactional-command-bus.d.ts +10 -0
  267. package/dist/kernel/transactional-command-bus.d.ts.map +1 -0
  268. package/dist/kernel/transactional-command-bus.js +17 -0
  269. package/dist/kernel/transactional-command-bus.js.map +1 -0
  270. package/dist/module/binding-options-of.d.ts +5 -0
  271. package/dist/module/binding-options-of.d.ts.map +1 -0
  272. package/dist/module/binding-options-of.js +23 -0
  273. package/dist/module/binding-options-of.js.map +1 -0
  274. package/dist/module/binding-options.d.ts +8 -0
  275. package/dist/module/binding-options.d.ts.map +1 -0
  276. package/dist/module/binding-options.js +2 -0
  277. package/dist/module/binding-options.js.map +1 -0
  278. package/dist/module/binding-target.d.ts +15 -0
  279. package/dist/module/binding-target.d.ts.map +1 -0
  280. package/dist/module/binding-target.js +2 -0
  281. package/dist/module/binding-target.js.map +1 -0
  282. package/dist/module/collect-wiring.d.ts +5 -0
  283. package/dist/module/collect-wiring.d.ts.map +1 -0
  284. package/dist/module/collect-wiring.js +13 -0
  285. package/dist/module/collect-wiring.js.map +1 -0
  286. package/dist/module/context-module.d.ts +10 -0
  287. package/dist/module/context-module.d.ts.map +1 -0
  288. package/dist/module/context-module.js +2 -0
  289. package/dist/module/context-module.js.map +1 -0
  290. package/dist/module/define-context-module.d.ts +4 -0
  291. package/dist/module/define-context-module.d.ts.map +1 -0
  292. package/dist/module/define-context-module.js +5 -0
  293. package/dist/module/define-context-module.js.map +1 -0
  294. package/dist/module/factory-made-handlers.d.ts +6 -0
  295. package/dist/module/factory-made-handlers.d.ts.map +1 -0
  296. package/dist/module/factory-made-handlers.js +24 -0
  297. package/dist/module/factory-made-handlers.js.map +1 -0
  298. package/dist/module/handler-target.d.ts +11 -0
  299. package/dist/module/handler-target.d.ts.map +1 -0
  300. package/dist/module/handler-target.js +2 -0
  301. package/dist/module/handler-target.js.map +1 -0
  302. package/dist/module/module-builder.d.ts +50 -0
  303. package/dist/module/module-builder.d.ts.map +1 -0
  304. package/dist/module/module-builder.js +2 -0
  305. package/dist/module/module-builder.js.map +1 -0
  306. package/dist/module/module-context.d.ts +5 -0
  307. package/dist/module/module-context.d.ts.map +1 -0
  308. package/dist/module/module-context.js +2 -0
  309. package/dist/module/module-context.js.map +1 -0
  310. package/dist/module/module-recorder.d.ts +31 -0
  311. package/dist/module/module-recorder.d.ts.map +1 -0
  312. package/dist/module/module-recorder.js +133 -0
  313. package/dist/module/module-recorder.js.map +1 -0
  314. package/dist/module/protected-use-case-target.d.ts +14 -0
  315. package/dist/module/protected-use-case-target.d.ts.map +1 -0
  316. package/dist/module/protected-use-case-target.js +2 -0
  317. package/dist/module/protected-use-case-target.js.map +1 -0
  318. package/dist/module/use-case-target.d.ts +8 -0
  319. package/dist/module/use-case-target.d.ts.map +1 -0
  320. package/dist/module/use-case-target.js +2 -0
  321. package/dist/module/use-case-target.js.map +1 -0
  322. package/dist/module/value-binding-options.d.ts +13 -0
  323. package/dist/module/value-binding-options.d.ts.map +1 -0
  324. package/dist/module/value-binding-options.js +2 -0
  325. package/dist/module/value-binding-options.js.map +1 -0
  326. package/dist/testing/create-test-application.d.ts +12 -0
  327. package/dist/testing/create-test-application.d.ts.map +1 -0
  328. package/dist/testing/create-test-application.js +89 -0
  329. package/dist/testing/create-test-application.js.map +1 -0
  330. package/dist/testing/create-test-kernel.d.ts +6 -0
  331. package/dist/testing/create-test-kernel.d.ts.map +1 -0
  332. package/dist/testing/create-test-kernel.js +65 -0
  333. package/dist/testing/create-test-kernel.js.map +1 -0
  334. package/dist/testing/fake.d.ts +5 -0
  335. package/dist/testing/fake.d.ts.map +1 -0
  336. package/dist/testing/fake.js +6 -0
  337. package/dist/testing/fake.js.map +1 -0
  338. package/dist/testing/test-application-of.d.ts +10 -0
  339. package/dist/testing/test-application-of.d.ts.map +1 -0
  340. package/dist/testing/test-application-of.js +49 -0
  341. package/dist/testing/test-application-of.js.map +1 -0
  342. package/dist/testing/test-application-options.d.ts +35 -0
  343. package/dist/testing/test-application-options.d.ts.map +1 -0
  344. package/dist/testing/test-application-options.js +2 -0
  345. package/dist/testing/test-application-options.js.map +1 -0
  346. package/dist/testing/test-application.d.ts +15 -0
  347. package/dist/testing/test-application.d.ts.map +1 -0
  348. package/dist/testing/test-application.js +2 -0
  349. package/dist/testing/test-application.js.map +1 -0
  350. package/dist/testing/test-jobs.d.ts +16 -0
  351. package/dist/testing/test-jobs.d.ts.map +1 -0
  352. package/dist/testing/test-jobs.js +2 -0
  353. package/dist/testing/test-jobs.js.map +1 -0
  354. package/dist/testing/test-kernel.d.ts +39 -0
  355. package/dist/testing/test-kernel.d.ts.map +1 -0
  356. package/dist/testing/test-kernel.js +2 -0
  357. package/dist/testing/test-kernel.js.map +1 -0
  358. package/dist/token/same-token.d.ts +3 -0
  359. package/dist/token/same-token.d.ts.map +1 -0
  360. package/dist/token/same-token.js +5 -0
  361. package/dist/token/same-token.js.map +1 -0
  362. package/dist/token/token-map.d.ts +14 -0
  363. package/dist/token/token-map.d.ts.map +1 -0
  364. package/dist/token/token-map.js +24 -0
  365. package/dist/token/token-map.js.map +1 -0
  366. package/dist/token/token-set.d.ts +9 -0
  367. package/dist/token/token-set.d.ts.map +1 -0
  368. package/dist/token/token-set.js +19 -0
  369. package/dist/token/token-set.js.map +1 -0
  370. package/dist/wiring/any-message-handler.d.ts +5 -0
  371. package/dist/wiring/any-message-handler.d.ts.map +1 -0
  372. package/dist/wiring/any-message-handler.js +2 -0
  373. package/dist/wiring/any-message-handler.js.map +1 -0
  374. package/dist/wiring/assert-port.d.ts +4 -0
  375. package/dist/wiring/assert-port.d.ts.map +1 -0
  376. package/dist/wiring/assert-port.js +10 -0
  377. package/dist/wiring/assert-port.js.map +1 -0
  378. package/dist/wiring/binding-source.d.ts +2 -0
  379. package/dist/wiring/binding-source.d.ts.map +1 -0
  380. package/dist/wiring/binding-source.js +2 -0
  381. package/dist/wiring/binding-source.js.map +1 -0
  382. package/dist/wiring/binding.d.ts +22 -0
  383. package/dist/wiring/binding.d.ts.map +1 -0
  384. package/dist/wiring/binding.js +2 -0
  385. package/dist/wiring/binding.js.map +1 -0
  386. package/dist/wiring/declarer.d.ts +6 -0
  387. package/dist/wiring/declarer.d.ts.map +1 -0
  388. package/dist/wiring/declarer.js +2 -0
  389. package/dist/wiring/declarer.js.map +1 -0
  390. package/dist/wiring/domain-event-handling.d.ts +20 -0
  391. package/dist/wiring/domain-event-handling.d.ts.map +1 -0
  392. package/dist/wiring/domain-event-handling.js +2 -0
  393. package/dist/wiring/domain-event-handling.js.map +1 -0
  394. package/dist/wiring/event-consumption.d.ts +17 -0
  395. package/dist/wiring/event-consumption.d.ts.map +1 -0
  396. package/dist/wiring/event-consumption.js +2 -0
  397. package/dist/wiring/event-consumption.js.map +1 -0
  398. package/dist/wiring/job-registration.d.ts +14 -0
  399. package/dist/wiring/job-registration.d.ts.map +1 -0
  400. package/dist/wiring/job-registration.js +2 -0
  401. package/dist/wiring/job-registration.js.map +1 -0
  402. package/dist/wiring/kind-aspect.d.ts +11 -0
  403. package/dist/wiring/kind-aspect.d.ts.map +1 -0
  404. package/dist/wiring/kind-aspect.js +2 -0
  405. package/dist/wiring/kind-aspect.js.map +1 -0
  406. package/dist/wiring/lifetime.d.ts +2 -0
  407. package/dist/wiring/lifetime.d.ts.map +1 -0
  408. package/dist/wiring/lifetime.js +2 -0
  409. package/dist/wiring/lifetime.js.map +1 -0
  410. package/dist/wiring/message-handling.d.ts +15 -0
  411. package/dist/wiring/message-handling.d.ts.map +1 -0
  412. package/dist/wiring/message-handling.js +2 -0
  413. package/dist/wiring/message-handling.js.map +1 -0
  414. package/dist/wiring/natural-kind.d.ts +9 -0
  415. package/dist/wiring/natural-kind.d.ts.map +1 -0
  416. package/dist/wiring/natural-kind.js +11 -0
  417. package/dist/wiring/natural-kind.js.map +1 -0
  418. package/dist/wiring/policy-binding.d.ts +11 -0
  419. package/dist/wiring/policy-binding.d.ts.map +1 -0
  420. package/dist/wiring/policy-binding.js +2 -0
  421. package/dist/wiring/policy-binding.js.map +1 -0
  422. package/dist/wiring/projection-registration.d.ts +22 -0
  423. package/dist/wiring/projection-registration.d.ts.map +1 -0
  424. package/dist/wiring/projection-registration.js +2 -0
  425. package/dist/wiring/projection-registration.js.map +1 -0
  426. package/dist/wiring/recipe.d.ts +9 -0
  427. package/dist/wiring/recipe.d.ts.map +1 -0
  428. package/dist/wiring/recipe.js +15 -0
  429. package/dist/wiring/recipe.js.map +1 -0
  430. package/dist/wiring/resolve.d.ts +3 -0
  431. package/dist/wiring/resolve.d.ts.map +1 -0
  432. package/dist/wiring/resolve.js +2 -0
  433. package/dist/wiring/resolve.js.map +1 -0
  434. package/dist/wiring/where-of.d.ts +9 -0
  435. package/dist/wiring/where-of.d.ts.map +1 -0
  436. package/dist/wiring/where-of.js +9 -0
  437. package/dist/wiring/where-of.js.map +1 -0
  438. package/dist/wiring/wiring-draft.d.ts +31 -0
  439. package/dist/wiring/wiring-draft.d.ts.map +1 -0
  440. package/dist/wiring/wiring-draft.js +83 -0
  441. package/dist/wiring/wiring-draft.js.map +1 -0
  442. package/dist/wiring/wiring.d.ts +23 -0
  443. package/dist/wiring/wiring.d.ts.map +1 -0
  444. package/dist/wiring/wiring.js +2 -0
  445. package/dist/wiring/wiring.js.map +1 -0
  446. package/package.json +42 -4
package/README.md CHANGED
@@ -1,3 +1,1076 @@
1
- # Temporary Holding Version
1
+ # @ddd-ts-kit/di
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Context modules on top of [tsyringe](https://github.com/microsoft/tsyringe), for projects that
4
+ want dependency injection without a heavy framework.
5
+
6
+ A port is an abstract class, and the class is what you inject by. A class says what it is, and
7
+ the context is assembled from what its classes say: there is no file listing bindings to keep in
8
+ step with the code, and no constant standing for a port. The explicit module, where every
9
+ binding is written by hand, stays supported and mixes with the declared style in one
10
+ application.
11
+
12
+ ```
13
+ npm i @ddd-ts-kit/di tsyringe reflect-metadata
14
+ ```
15
+
16
+ `reflect-metadata` is imported by this package before tsyringe loads (tsyringe refuses to load
17
+ without it), so you do not import it yourself. No decorator of the kit needs it: dependencies
18
+ are named, never reflected.
19
+
20
+ ## A context that declares itself
21
+
22
+ The decorators come from `@ddd-ts-kit/kernel/declarations`, which holds no container: the
23
+ application layer imports the kernel and nothing else. This package turns the declarations
24
+ into bindings; nothing else does. The domain carries no decorator.
25
+
26
+ A port lives where the layer that owns it lives: a repository port in
27
+ `domain/repositories/`, any other port in `application/ports/`, one abstract class per file.
28
+
29
+ ```ts
30
+ // contexts/billing/domain/repositories/invoice.repository.ts
31
+ import { Repository } from '@ddd-ts-kit/kernel';
32
+
33
+ export abstract class InvoiceRepository extends Repository<Invoice> {
34
+ abstract findUnpaidOf(customerId: string): Promise<readonly Invoice[]>;
35
+ }
36
+ ```
37
+
38
+ ```ts
39
+ // contexts/billing/application/ports/payment-gateway.port.ts
40
+ export abstract class PaymentGateway {
41
+ abstract capture(invoiceId: string, amount: Money): Promise<PaymentReceipt>;
42
+ }
43
+ ```
44
+
45
+ ```ts
46
+ // contexts/billing/application/commands/issue-invoice/issue-invoice.command.ts
47
+ import { AuthorizedCommand, type Actor } from '@ddd-ts-kit/kernel';
48
+ import { Requires } from '@ddd-ts-kit/kernel/declarations';
49
+
50
+ @Requires(permissions['invoice.issue'], {
51
+ resource: (command: IssueInvoice) => ({ type: 'customer', id: command.customerId }),
52
+ })
53
+ export class IssueInvoice extends AuthorizedCommand<string> {
54
+ constructor(actor: Actor, readonly customerId: string) {
55
+ super(actor);
56
+ }
57
+ }
58
+ ```
59
+
60
+ ```ts
61
+ // contexts/billing/application/commands/issue-invoice/issue-invoice.handler.ts
62
+ import { Clock, IdGenerator } from '@ddd-ts-kit/kernel';
63
+ import { CommandHandler } from '@ddd-ts-kit/kernel/declarations';
64
+
65
+ @CommandHandler(IssueInvoice, [InvoiceRepository, PaymentGateway, IdGenerator, Clock])
66
+ export class IssueInvoiceHandler implements CommandHandler<IssueInvoice> {
67
+ constructor(
68
+ private readonly invoices: InvoiceRepository,
69
+ private readonly payments: PaymentGateway,
70
+ private readonly ids: IdGenerator,
71
+ private readonly clock: Clock,
72
+ ) {}
73
+
74
+ async execute(command: IssueInvoice): Promise<string> {
75
+ const invoice = Invoice.issue({ id: this.ids.next(), customerId: command.customerId, issuedAt: this.clock.now() });
76
+
77
+ await this.invoices.save(invoice);
78
+
79
+ return invoice.id;
80
+ }
81
+ }
82
+ ```
83
+
84
+ The handler only saves. The save hands over the domain events the aggregate recorded, and the
85
+ unit of work of the command dispatches them once the handler has returned, before the commit:
86
+ see "Domain events and the unit of work".
87
+
88
+ ```ts
89
+ // contexts/billing/application/queries/get-invoice/get-invoice.query.ts and .handler.ts
90
+ @Public()
91
+ export class GetInvoice extends Query<InvoiceView | undefined> {
92
+ constructor(readonly invoiceId: string) {
93
+ super();
94
+ }
95
+ }
96
+
97
+ @QueryHandler(GetInvoice, [InvoiceRepository])
98
+ export class GetInvoiceHandler implements QueryHandler<GetInvoice> { /* ... */ }
99
+ ```
100
+
101
+ ```ts
102
+ // contexts/billing/application/integration/handlers/invoice-on-order-placed.integration-handler.ts
103
+ import { CommandBus, systemActor, type IntegrationEvent } from '@ddd-ts-kit/kernel';
104
+ import { IntegrationEventHandler } from '@ddd-ts-kit/kernel/declarations';
105
+ import { OrderPlaced, type OrderPlacedPayload } from '@/contexts/ordering/published-language/index.js';
106
+
107
+ @IntegrationEventHandler(OrderPlaced, { consumer: 'billing.invoice-on-order-placed' }, [CommandBus])
108
+ export class InvoiceOnOrderPlaced implements IntegrationEventHandler<OrderPlacedPayload> {
109
+ constructor(private readonly commands: CommandBus) {}
110
+
111
+ async handle(event: IntegrationEvent<OrderPlacedPayload>): Promise<void> {
112
+ await this.commands.execute(new IssueInvoice(systemActor('billing.invoice-on-order-placed'), event.payload.customerId));
113
+ }
114
+ }
115
+ ```
116
+
117
+ ```ts
118
+ // contexts/billing/application/integration/publishers/invoice-issued.publisher.ts
119
+ import { IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
120
+ import { DomainEventHandler } from '@ddd-ts-kit/kernel/declarations';
121
+
122
+ @DomainEventHandler(InvoiceIssued, { name: 'billing.invoice.issued' }, [IntegrationEventPublisher, IdGenerator])
123
+ export class InvoiceIssuedPublisher implements DomainEventHandler<InvoiceIssued> {
124
+ constructor(
125
+ private readonly publisher: IntegrationEventPublisher,
126
+ private readonly ids: IdGenerator,
127
+ ) {}
128
+
129
+ async handle(event: InvoiceIssued): Promise<void> { /* publishes billing.invoice.issued */ }
130
+ }
131
+ ```
132
+
133
+ ```ts
134
+ // contexts/billing/infrastructure/persistence/knex-invoice.repository.ts
135
+ import { Clock, ExecutionScope } from '@ddd-ts-kit/kernel';
136
+ import { Repository } from '@ddd-ts-kit/kernel/declarations';
137
+
138
+ @Repository(InvoiceRepository, [Database, ExecutionScope, Clock])
139
+ export class KnexInvoiceRepository implements InvoiceRepository { /* ... */ }
140
+ ```
141
+
142
+ ```ts
143
+ // contexts/billing/infrastructure/external/http-payment.gateway.ts
144
+ import { Gateway } from '@ddd-ts-kit/kernel/declarations';
145
+ import { retry, timeout } from '@ddd-ts-kit/aspects';
146
+
147
+ @Gateway(PaymentGateway, { aspects: [retry({ attempts: 3 }), timeout(2000)] }, [PaymentSettings])
148
+ export class HttpPaymentGateway implements PaymentGateway { /* ... */ }
149
+ ```
150
+
151
+ That is the whole context. It is assembled in one line, in one of two ways.
152
+
153
+ ```ts
154
+ // main.ts, discovered: no file of the context names its classes
155
+ import { CommandBus } from '@ddd-ts-kit/kernel';
156
+ import { createApplication, kernelModule } from '@ddd-ts-kit/di';
157
+ import { discoverContext } from '@ddd-ts-kit/di/discovery';
158
+
159
+ const billing = await discoverContext('billing', new URL('./contexts/billing/', import.meta.url));
160
+ const app = createApplication({ modules: [kernelModule(), platformModule, billing], authorization: 'required' });
161
+ await app.start();
162
+ const invoiceId = await app.resolve(CommandBus).execute(new IssueInvoice(actor, 'customer-1')); // typed string
163
+ ```
164
+
165
+ ```ts
166
+ // main.ts, listed: one generated barrel per context and nothing else
167
+ import { contextModule } from '@ddd-ts-kit/di';
168
+ import * as billing from '@/contexts/billing/billing.declarations.js';
169
+
170
+ const app = createApplication({ modules: [kernelModule(), platformModule, contextModule('billing', billing)] });
171
+ ```
172
+
173
+ ## Ports and values
174
+
175
+ ### A port is an abstract class
176
+
177
+ The class names the dependency and carries its type: a dependency list is checked against a
178
+ constructor, `app.resolve(Clock)` answers a `Clock`, and nothing else stands for the port. The
179
+ ports of the kernel (`Clock`, `IdGenerator`, `UnitOfWork`, `CommandBus`, `QueryBus`,
180
+ `IntegrationEventPublisher`, `ExecutionScope`...) are abstract classes imported from
181
+ `@ddd-ts-kit/kernel`; yours are written the same way.
182
+
183
+ The rule, checked when a binding is registered (`INVALID_PORT` for `toClass`, a
184
+ `DeclarationError` for a decorator):
185
+
186
+ - a port has abstract members only: no body, no constructor parameter, no private or protected
187
+ member. One exception, `static readonly tokenKey` (see "Identity");
188
+ - a port may extend another port, and is then another port;
189
+ - an adapter `implements` its port and never `extends` it;
190
+ - a class bound to itself (`bind(Settings).toClass(Settings, [...])`, a handler, a job, a
191
+ policy) is no port and keeps its methods.
192
+
193
+ Because a port holds nothing but a shape, a fake and a plain object stand for it:
194
+ `fake(Clock, { now: () => new Date(0) })`.
195
+
196
+ ### A value is typed by an abstract class too
197
+
198
+ A configuration object, a database handle, a list, a primitive: an abstract class with
199
+ `abstract readonly` properties, bound with `toValue` to a plain object. That is the one way.
200
+
201
+ ```ts
202
+ // platform/database.ts
203
+ export abstract class Database {
204
+ abstract readonly client: Knex;
205
+ }
206
+
207
+ // contexts/billing/application/ports/payment-settings.port.ts
208
+ export abstract class PaymentSettings {
209
+ abstract readonly apiUrl: string;
210
+ abstract readonly apiKey: string;
211
+ }
212
+
213
+ // platform/platform.module.ts
214
+ builder.bind(Database).toValue({ client: knex(config.database) });
215
+ builder.bind(PaymentSettings).toValue({ apiUrl: config.payments.url, apiKey: config.payments.key });
216
+ ```
217
+
218
+ A primitive is wrapped: `abstract class TaxRate { abstract readonly value: number }`, then
219
+ `builder.bind(TaxRate).toValue({ value: 0.2 })`.
220
+
221
+ ### What the compiler checks
222
+
223
+ Where the declaration or the statement is written, with no run:
224
+
225
+ | Mistake | Example |
226
+ |---|---|
227
+ | the class does not implement the port | `bind(Mailer).toClass(SmsSender)`, `@Repository(InvoiceRepository)` on a class missing `findById` |
228
+ | the value does not have the shape of the port | `bind(Clock).toValue('noon')`, `fake(OrderRepository, { save })`, `override(Clock, 12)` |
229
+ | the list names a port of another type | `handledBy(GetOrderHandler, [Clock])` for a constructor taking an `OrderRepository` |
230
+ | the list is in another order than the constructor | `[IdGenerator, Clock]` for `(clock: Clock, ids: IdGenerator)` |
231
+ | the list is missing, one short or one too many | `toClass(ReserveStock)` for a constructor with parameters |
232
+ | the list names a concrete class where its port is expected | `[InMemoryOrderRepository]` for a parameter typed `OrderRepository` |
233
+ | the handler does not handle that message or answers another result | `command(PlaceOrder).handledBy(CancelOrderHandler, ...)` |
234
+ | the event handler handles another payload | `on(ParcelShipped, ...).handledBy(InvoiceOnOrderPlaced, ...)` |
235
+ | `.requires(...)` on a message that carries no actor | `query(GetCatalog).requires(permission)` |
236
+
237
+ Two limits of a structural type system. A concrete class is told apart from its port by what
238
+ it adds: a class with no member of its own, private ones included, is the port for the
239
+ compiler. And a constructor whose parameters are all optional still has parameters: it takes a
240
+ dependency list.
241
+
242
+ ### Identity
243
+
244
+ `tokenIdentity(port)` is the symbol a container keys a port by; `describeToken(port)` is its
245
+ class name, which is what error messages and `describe()` print.
246
+
247
+ - A port of your project is identified by the class itself. Two classes of the same name are
248
+ two ports, and a class that extends a port is another port.
249
+ - A port that declares `static readonly tokenKey = '<package>.<Port>'` is identified by that
250
+ key, the same in every copy of its package a process loads. The ports of the kernel all
251
+ declare one (`kernel.Clock`...), and so do `UseCaseGuard`, `UseCaseDeclarations` and
252
+ `DeclaredPolicies` of this package: a handler compiled against one copy of the kernel is
253
+ wired by a container built on another.
254
+ - The key is not inherited: `abstract class BusinessClock extends Clock` is another port, bound
255
+ separately.
256
+
257
+ The limit: a port of your own without a `tokenKey`, loaded twice (the same file imported as
258
+ source and as compiled output, or a package installed in two versions), is two ports. The
259
+ binding made for one does not answer the other, which the application reports as unbound.
260
+ Load each port once, or give a port that a published package exposes a `tokenKey`.
261
+
262
+ ## What a class may declare
263
+
264
+ The full list, with the options of each, is in the README of `@ddd-ts-kit/kernel`
265
+ ("Declarations"). What this package does with each:
266
+
267
+ | Declaration | Becomes |
268
+ |---|---|
269
+ | `@Requires(permission, options?)` / `@Public()` on a command or a query | the access of the use case: what `.requires(...)` and `.public()` say in an explicit module, read by `authorization: 'required'` and by the authorization module |
270
+ | `@CommandHandler(Message, [ports])`, `@QueryHandler(Message, [ports])` | a singleton of kind `use-case`, registered on `CommandBus` or `QueryBus` |
271
+ | `@IntegrationEventHandler(contract, { consumer }, [ports])` | a singleton of kind `consumer`, subscribed by `start()` on the subscriber of its context, idempotent per consumer name, run in the execution context of the event, its mark and its work in one unit of work |
272
+ | `@DomainEventHandler(Event, { name, phase? }, [ports])` | a singleton of kind `domain-event-handler`, attached to `DomainEventDispatcher` when the application is created, in its phase: `before-commit` (default), at the end of the unit of work and inside its transaction, or `after-commit`, once it has committed; an event dispatched under the name and built from another class is left alone |
273
+ | `@Projection(name, [ports])`, with `@On(definition)` or `@On(Event, { name })` on its methods | a singleton of kind `consumer`. Each integration event it applies is subscribed by `start()` under the name of the projection, exactly as a handler of integration events is: its own execution context, idempotent per name, its mark and its write in one unit of work. Each domain event of its own context is attached to `DomainEventDispatcher` and applied inside the save. See "Projections" |
274
+ | `@Scheduled(name, schedule and options, [ports])` | a singleton of kind `job`, registered on `JobScheduler` when the application is created, started by `start()` after the consumers and stopped first by `stop()`; see "Scheduled jobs" |
275
+ | `@Repository(Port, options?, [ports])` | the binding of the port, kind `repository`: its save hands the domain events of the aggregate to the unit of work, which dispatches them at its end |
276
+ | `@Gateway(Port, options?, [ports])` | the binding of the port, kind `gateway` |
277
+ | `@Adapter(Port, { kind? }, [ports])` | the binding of the port, kind `external` (default) or `publisher` |
278
+ | `@Policy(permission, { name }, [ports])` | a singleton listed in `DeclaredPolicies`, which the authorization module evaluates |
279
+
280
+ A handler, a job, a policy and an event handler are bound under themselves: the class is what
281
+ builds them, and `app.resolve(IssueInvoiceHandler)` answers the instance the bus runs. One
282
+ class is therefore one binding: listing a declared class in two modules is refused.
283
+
284
+ The options of a repository, a gateway and an adapter are the ones of an explicit binding:
285
+ `transient: true` for `.transient()`, `aspects: [...]` for `.with(...)`, the first outermost,
286
+ and `replaces: true` for `rebind`, which a platform adapter uses to take the place of a default
287
+ of the kernel module (`@Adapter(UnitOfWork, { replaces: true }, [Database])`). The kind
288
+ reaches the `decorate` hook, so tracing and global aspects see declared adapters as they see
289
+ explicit ones. A handler is built on the first message it handles; nothing is built when a
290
+ class is declared or when a module is created.
291
+
292
+ ### `contextModule(name, declared)`
293
+
294
+ `declared` is a class, a list (lists may nest) or the namespace import of a barrel. Of a
295
+ namespace only the declared classes are kept, so the barrel may also export ports and types.
296
+ A class named directly in a list must carry a declaration (`UNDECLARED_CLASS`): a forgotten
297
+ decorator is not skipped in silence. A class met twice is taken once. The module is static:
298
+ a bundler follows every class, and so does a dead-code analysis.
299
+
300
+ ### `discoverContext(name, directory, options?)`
301
+
302
+ From `@ddd-ts-kit/di/discovery`, the one entry of this package that reads the disk. It loads,
303
+ in the order of their paths, every file under `application/` and `infrastructure/` of the
304
+ context whose name ends with a role suffix, and keeps the exported classes that declare
305
+ themselves:
306
+
307
+ | Suffix | Folder | Declares |
308
+ |---|---|---|
309
+ | `*.command`, `*.query` | `application/commands/`, `application/queries/` | `@Requires` or `@Public` |
310
+ | `*.handler` | `application/commands/`, `application/queries/` | `@CommandHandler` or `@QueryHandler` |
311
+ | `*.integration-handler` | `application/integration/handlers/` | `@IntegrationEventHandler` |
312
+ | `*.publisher` | `application/integration/publishers/` | `@DomainEventHandler` |
313
+ | `*.event-handler` | `application/event-handlers/` | `@DomainEventHandler` |
314
+ | `*.policy` | `application/authorization/` | `@Policy` |
315
+ | `*.job` | `application/scheduling/` | `@Scheduled` |
316
+ | `*.projection` | `application/projections/` | `@Projection` |
317
+ | `*.repository` | `infrastructure/persistence/` | `@Repository` |
318
+ | `*.gateway`, `*.adapter` | `infrastructure/` | `@Gateway`, `@Adapter` |
319
+
320
+ A publisher is a domain event handler whose work is to announce the event to the other
321
+ contexts: the same declaration as an event handler, under a suffix and a folder that say what
322
+ it is for.
323
+
324
+ `domain/`, `presentation/`, `published-language/` and `test/` are never read, and neither are
325
+ the `*.port` files of `application/ports/`: ports and fakes are left alone. It fails with a
326
+ `WiringError` naming the file:
327
+
328
+ - `DISCOVERY_FAILED`: a file that throws while loading (the failure is kept as `cause`), or a
329
+ directory with neither `application/` nor `infrastructure/`;
330
+ - `NOTHING_DECLARED`: a file of a role suffix that exports no declared class;
331
+ - `MISPLACED_DECLARATION`: a file of a role suffix outside its folder (refused before it is
332
+ loaded), or a class that declares another role than the one of its file.
333
+
334
+ Options: `ignore: (file) => boolean` leaves files out (a policy written with `definePolicy`, an
335
+ adapter bound explicitly); `extensions` (default `.js`, `.mjs`, `.ts`, `.mts`, the first that
336
+ exists wins, `.d.ts` never) says what is loaded; `load: (url) => Promise<module>` replaces
337
+ `import()`.
338
+
339
+ What it costs, to weigh against one generated barrel:
340
+
341
+ - it reads directories and imports at startup, sequentially, before the application exists;
342
+ - a bundler cannot follow it: a bundled application lists its classes with `contextModule`;
343
+ - the files it loads are imported by nothing, so knip and dependency-cruiser must be told that
344
+ they are entries (see "Limits");
345
+ - it runs what runs the process: compiled `.js` in production, `.ts` only under a loader or a
346
+ test runner that transforms them;
347
+ - under Vitest this package must be inlined, so that the `import()` of discovery goes through
348
+ the runner, which compiles the `.ts` role files and lowers their decorators:
349
+ `test: { server: { deps: { inline: ['@ddd-ts-kit/di'] } } }`. A project initialised with
350
+ `--assembly discovery` has that line in its `vitest.config.ts`.
351
+
352
+ Prefer `contextModule` with a barrel for anything bundled, for a serverless cold start, and
353
+ whenever the tooling should see the classes; prefer discovery when adding a file must be all
354
+ it takes.
355
+
356
+ ## Decorator modes
357
+
358
+ The declarations work under both decorator modes of TypeScript, with the same source line:
359
+
360
+ | Stack | `tsconfig.json` | Why |
361
+ |---|---|---|
362
+ | class-validator, NestJS, TypeORM entities, any library with legacy decorators | `"experimentalDecorators": true` | those libraries only have legacy decorators, and one program has one mode |
363
+ | anything else | nothing: leave `experimentalDecorators` unset | standard (TC39) decorators need no flag |
364
+
365
+ A declaration decorator is a function of the class alone. The legacy call hands it the class,
366
+ the standard call hands it the class and a context that it does not read, so the same
367
+ function serves both and stores the declaration in a registry of the kernel, never in
368
+ `Symbol.metadata`. Neither mode needs `emitDecoratorMetadata`, and no parameter decorator
369
+ exists in either: the dependency list is the one mechanism. Vitest lowers legacy decorators by
370
+ itself; for standard ones add `decorators()` of `@ddd-ts-kit/testing/vitest` to the Vitest
371
+ config.
372
+
373
+ ## An explicit module
374
+
375
+ The alternative: every binding written by hand in `<context>.module.ts`. It needs no
376
+ decorator at all, and suits a context whose wiring is unusual (factories, values, conditional
377
+ bindings) or a team that wants the wiring in one file. The ports are the same abstract classes,
378
+ in the same files.
379
+
380
+ ```ts
381
+ // contexts/ordering/ordering.module.ts
382
+ import { Clock, CommandBus, IdGenerator, IntegrationEventPublisher } from '@ddd-ts-kit/kernel';
383
+ import { defineContextModule } from '@ddd-ts-kit/di';
384
+ import { PaymentCaptured } from '@/contexts/billing/published-language/index.js';
385
+
386
+ export const orderingModule = defineContextModule({
387
+ name: 'ordering',
388
+ register(builder) {
389
+ builder.bind(OrderRepository).toClass(DrizzleOrderRepository, [Database]).as('repository');
390
+ builder.bind(OrderReads).toClass(DrizzleOrderReads, [Database]).as('repository');
391
+ builder.bind(StockGateway).toClass(HttpStockGateway, [StockSettings]).as('gateway');
392
+
393
+ builder
394
+ .command(PlaceOrder)
395
+ .handledBy(PlaceOrderHandler, [OrderRepository, StockGateway, IntegrationEventPublisher, IdGenerator, Clock]);
396
+ builder.command(MarkOrderPaid).handledBy(MarkOrderPaidHandler, [OrderRepository, Clock]);
397
+ builder.query(GetOrder).handledBy(GetOrderHandler, [OrderReads]);
398
+
399
+ builder
400
+ .on(PaymentCaptured, { consumer: 'ordering.mark-order-paid' })
401
+ .handledBy(MarkOrderPaidOnPaymentCaptured, [CommandBus]);
402
+ },
403
+ });
404
+ ```
405
+
406
+ Nothing else is needed: no manual registration on a bus, no subscription code.
407
+
408
+ ```ts
409
+ // main.ts
410
+ import { CommandBus } from '@ddd-ts-kit/kernel';
411
+ import { createApplication, kernelModule } from '@ddd-ts-kit/di';
412
+
413
+ const app = createApplication({ modules: [kernelModule(), platformModule, orderingModule, billingModule] });
414
+ await app.start();
415
+ const orderId = await app.resolve(CommandBus).execute(new PlaceOrder('sku-1')); // typed string
416
+ process.on('SIGTERM', () => void app.stop());
417
+ ```
418
+
419
+ ```ts
420
+ // contexts/ordering/test/unit/place-order.handler.spec.ts
421
+ import { createTestApplication, fake } from '@ddd-ts-kit/di';
422
+
423
+ it('places an order and announces it', async () => {
424
+ const orders = new InMemoryOrderRepository();
425
+ const app = createTestApplication({
426
+ modules: [orderingModule],
427
+ fakes: [fake(OrderRepository, orders), fake(StockGateway, new InMemoryStock({ 'sku-1': 3 }))],
428
+ });
429
+
430
+ const orderId = await app.commandBus.execute(new PlaceOrder('sku-1'));
431
+
432
+ expect(orders.count()).toBe(1);
433
+ expect(app.eventBus.published).toMatchObject([{ type: 'ordering.order.placed', payload: { orderId } }]);
434
+ });
435
+ ```
436
+
437
+ ## Mixing the two styles
438
+
439
+ - **In one application**: declared and explicit modules are listed together; every check
440
+ below applies across them.
441
+ - **In one module**: `builder.include(declared)` adds declared classes next to explicit
442
+ statements, for the value or the factory a decorator cannot express.
443
+
444
+ ```ts
445
+ export const billingModule = defineContextModule({
446
+ name: 'billing',
447
+ register(builder) {
448
+ builder.include(billing);
449
+ builder.bind(TaxRate).toValue({ value: config.taxRate });
450
+ },
451
+ });
452
+ ```
453
+
454
+ - **A decorated message in an explicit module**: `builder.command(IssueInvoice).handledBy(...)`
455
+ reads the `@Requires` or `@Public` of the message; `.requires(...)` and `.public()` written
456
+ on the builder win.
457
+ - **Replacing a declared adapter**: a later module calls `builder.rebind(Port)`, and a test
458
+ gives `fake(Port, ...)`, exactly as for an explicit binding.
459
+
460
+ ## Statements of an explicit module
461
+
462
+ | Call | Meaning |
463
+ |---|---|
464
+ | `bind(Port).toClass(Class, [ports])` | `new Class(...)` with the ports resolved in order; the list is omitted for a constructor without parameters. The class implements the port (`INVALID_PORT` when the port has a body or the class extends it) |
465
+ | `bind(Port).toFactory((resolve) => instance)` | anything a constructor cannot express |
466
+ | `bind(Port).toValue(value)` | an existing object, a fake, or the plain object a value class types |
467
+ | `.singleton()` / `.transient()` | one instance per application (default) or one per resolution |
468
+ | `.as(kind)` | role of the binding, handed to the `decorate` hook: `repository`, `gateway`, `external`, `use-case`, `publisher`, `consumer`, `domain-event-handler`, `job`. A port extending the kernel `Repository` is of kind `repository` without it |
469
+ | `rebind(Port).to...` | replaces the binding of a module listed earlier, such as a default of `kernelModule()` |
470
+ | `command(Type).handledBy(Handler, [ports])` | binds the handler under itself (singleton, kind `use-case`) and registers it on `CommandBus` |
471
+ | `query(Type).handledBy(Handler, [ports])` | the same on `QueryBus` |
472
+ | `on(contract, { consumer }).handledBy(Handler, [ports])` | binds the integration event handler under itself (kind `consumer`) and subscribes its `handle(event)` on the subscriber of the module |
473
+ | `job(name, schedule and options).handledBy(Job, [ports])` | binds the job under itself (singleton, kind `job`) and registers its `run(execution)` on `JobScheduler` |
474
+ | `.handledByFactory((resolve) => handler)` | the factory form of the four lines above, bound under a class made for the statement, which reads `command(PlaceOrder) handler` in a description |
475
+ | `include(declared)` | adds classes that declare themselves: what `contextModule` does |
476
+
477
+ **One class is one binding.** `handledBy(Handler)` binds the class under itself, so the same
478
+ handler class cannot be given to two statements: the second is refused with
479
+ `DUPLICATE_BINDING`. One handler per message is the rule the kit follows everywhere; where one
480
+ object really serves two statements, give each `handledByFactory(() => shared)`.
481
+
482
+ Handlers are registered when the application is created, which resolves the two buses; a
483
+ handler itself is built on the first message it handles, then reused. Consumers are subscribed
484
+ by `start()` and unsubscribed by `stop()`, since a subscription is a side effect.
485
+
486
+ ## Integration event handlers
487
+
488
+ `contract` is anything with `type`, `version` and `parse(event)`: a declaration made by
489
+ `defineIntegrationEvent` of `@ddd-ts-kit/events` fits as it is. An event of another version is
490
+ skipped, an event the contract cannot read throws `UnreadableEventError` so that the provider
491
+ retries or dead-letters it, and a redelivered event is handled once per `consumer` name when
492
+ `ProcessedEventStore` is bound. The name is required: it is the idempotency key, and it must
493
+ survive the renaming of a class.
494
+
495
+ ### The execution context of the event
496
+
497
+ Every consumer handles its events inside `ExecutionScope`, in a context of its own: origin
498
+ `event`, the correlation of the event (its id when it names none), the event as `causationId`,
499
+ and `systemActor(consumer)` as actor, which is therefore what `ActorContext` answers to the
500
+ repositories of the consuming context. Nothing to wire. A project whose envelopes name an
501
+ actor adds `on(contract, { consumer, actorOf: (event) => actor })`. For the correlation to
502
+ start at the request, publish through a scoped publisher:
503
+
504
+ ```ts
505
+ kernelModule({
506
+ eventPublisher: (resolve) => scopedPublisher(new OutboxPublisher(store), resolve(ExecutionScope)),
507
+ });
508
+ ```
509
+
510
+ ### One unit of work per event
511
+
512
+ When `UnitOfWork` and `ProcessedEventStore` are both bound, which `kernelModule()` does, the
513
+ idempotency mark and the work of the handler run inside one `unitOfWork.run`, opened inside
514
+ the execution context of the event:
515
+
516
+ - a handler that fails leaves no mark: the transaction that held it is rolled back, and the
517
+ redelivery is handled;
518
+ - a handler that succeeds commits its mark with its writes: the redelivery is skipped, and
519
+ there is no window where one is kept without the other;
520
+ - a command the handler sends runs its own `unitOfWork.run` inside that one, so the
521
+ `UnitOfWork` adapter must join the transaction that is open, as the adapters of the kit do;
522
+ - the unit of work is resolved at each event, as the command bus resolves it at each command.
523
+
524
+ This holds for a store whose mark is written in the database of the unit of work. Without a
525
+ `UnitOfWork` bound, the mark is taken first and given back with `release` when the handler
526
+ fails, for a store that has one; a store without `release` keeps the mark of a failed handler.
527
+
528
+ ### The subscriber of each context
529
+
530
+ A handler of a context is subscribed on `EventSubscribers.forContext(<module name>)`. By
531
+ default every context gets the single `IntegrationEventSubscriber`, which is the in-memory
532
+ event bus: one process, one bus, nothing to say.
533
+
534
+ A broker gives each consuming context its own subscriber, so that the context keeps its queues
535
+ when it is extracted. Say it once, in the kernel module or from a later module:
536
+
537
+ ```ts
538
+ kernelModule({ eventBus: bus, eventSubscribers: { forContext: (name) => bus.forConsumer(name) } });
539
+
540
+ builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
541
+ ```
542
+
543
+ The name handed to `forContext` is the name of the module: the first argument of
544
+ `contextModule` and `discoverContext`, the `name` of `defineContextModule`. An application
545
+ without the kernel module needs one of the two bound, `IntegrationEventSubscriber` or
546
+ `EventSubscribers`; `EventSubscribers` wins when both are.
547
+
548
+ ## Projections
549
+
550
+ A projection is a class of `application/projections/`, in a `*.projection.ts` file, declared
551
+ with `@Projection('<context>.<name>', [ports])`; the README of `@ddd-ts-kit/kernel`
552
+ ("Projections") says what it may do and why its writes are idempotent and ordered. Here it is
553
+ wired: `contextModule` and `discoverContext` find it like any declared class, and an explicit
554
+ module takes it with `builder.include(AppointmentListProjection)`.
555
+
556
+ The CLI does not know projections yet: `ddd-ts-kit check` refuses `application/projections/`
557
+ (`tree/closed`) unless the directory is declared in `extraDirectories`, and refuses a line for
558
+ a projection in the generated barrel (`declarations/barrel-current`). Until it does, a
559
+ project held by the CLI registers a projection by discovery or with `builder.include(...)`.
560
+
561
+ ```ts
562
+ const app = createTestApplication({ modules: [contextModule('reporting', reporting)] });
563
+
564
+ await app.start();
565
+ await app.eventBus.publish([orderPlaced('evt-1'), orderPlaced('evt-1'), orderPlaced('evt-2')]);
566
+
567
+ expect(await app.get(CustomerOrders).get('customer-1')).toMatchObject({ orders: 2 });
568
+ ```
569
+
570
+ A spec feeds a projection through the in-memory bus and reads the model through its port:
571
+ there is nothing else to learn. The fake of a read model port is `InMemoryReadModelStore` of
572
+ the kernel, bound by its adapter or given with `fake(Port, store)`.
573
+
574
+ - **One consumer.** The name of the projection is the consumer name of every event it applies:
575
+ the idempotency scope in `ProcessedEventStore`, the system actor it runs as, the
576
+ `process <projection>` span and, behind a broker, its queue or its group. Two projections of
577
+ the same name are refused (`DUPLICATE_PROJECTION`), and so are two methods of one projection
578
+ applying the same event (`DUPLICATE_CONSUMER`).
579
+ - **One unit of work per event.** With `UnitOfWork` and `ProcessedEventStore` bound to the same
580
+ database as the read model, the mark and the row commit or roll back together. Without
581
+ them, the stamp each row carries still makes a redelivery a no-op.
582
+ - **Measured.** With `Metrics` bound, `projection_events_total` counts each event by `context`,
583
+ `projection`, `event` and `result` (`applied`, `stale`, `ignored`, `failed`), and
584
+ `projection_last_event_age_ms` holds the age of the last event applied when it was.
585
+ - **Described.** `describe().projections` lists each projection with the events it applies and
586
+ the method of each; they do not appear among `integrationEventHandlers` and
587
+ `domainEventHandlers`.
588
+
589
+ ## Behind a broker
590
+
591
+ Discovery, a broker and a database, with no helper of your own. The complete composition root:
592
+
593
+ ```ts
594
+ // main.ts
595
+ import { EventSubscribers, ProcessedEventStore, UnitOfWork } from '@ddd-ts-kit/kernel';
596
+ import { createApplication, defineContextModule, kernelModule } from '@ddd-ts-kit/di';
597
+ import { discoverContext } from '@ddd-ts-kit/di/discovery';
598
+ import { RabbitMqEventBus } from '@ddd-ts-kit/rabbitmq';
599
+
600
+ const bus = new RabbitMqEventBus({ connection: config.amqpUrl });
601
+
602
+ const platformModule = defineContextModule({
603
+ name: 'platform',
604
+ register(builder) {
605
+ builder.bind(Database).toValue({ client: knex(config.database) });
606
+ builder.rebind(UnitOfWork).toClass(DatabaseUnitOfWork, [Database]);
607
+ builder.rebind(ProcessedEventStore).toClass(DatabaseProcessedEvents, [Database]);
608
+ builder.rebind(EventSubscribers).toValue({ forContext: (name) => bus.forConsumer(name) });
609
+ },
610
+ onStop: () => bus.close(),
611
+ });
612
+
613
+ const app = createApplication({
614
+ modules: [
615
+ kernelModule({ eventBus: bus }),
616
+ platformModule,
617
+ await discoverContext('ordering', new URL('./contexts/ordering/', import.meta.url)),
618
+ await discoverContext('billing', new URL('./contexts/billing/', import.meta.url)),
619
+ ],
620
+ authorization: 'required',
621
+ });
622
+
623
+ await app.start();
624
+ process.on('SIGTERM', () => void app.stop());
625
+ ```
626
+
627
+ That is all: `discoverContext('billing', ...)` names the module `billing`, so its integration
628
+ event handlers are subscribed on `bus.forConsumer('billing')`, each event handled in the
629
+ execution context of the event, its mark and its work in one database transaction. With a
630
+ barrel instead of discovery the two lines become `contextModule('ordering', ordering)` and
631
+ `contextModule('billing', billing)`, and nothing else changes. `DatabaseUnitOfWork` and
632
+ `DatabaseProcessedEvents` stand for the adapters of your persistence package; any bus with a
633
+ `forConsumer(name)` fits where `RabbitMqEventBus` is.
634
+
635
+ ## The kernel module
636
+
637
+ `kernelModule(options?)` binds the ports every context depends on.
638
+
639
+ | Port | Default | Option |
640
+ |---|---|---|
641
+ | `Clock` | `SystemClock` | `clock` |
642
+ | `IdGenerator` | `UuidGenerator` | `ids` |
643
+ | `UnitOfWork` | `InMemoryUnitOfWork` | `unitOfWork` |
644
+ | `CommandBus` | `InMemoryCommandBus` running each command in the unit of work: the domain events its saves announce are dispatched once the handler has returned, before the commit, then the `after-commit` handlers run | `commandBus` |
645
+ | `QueryBus` | `InMemoryQueryBus` | `queryBus` |
646
+ | `EventBus` | `InMemoryEventBus` | `eventBus` |
647
+ | `IntegrationEventPublisher` | the event bus | `eventPublisher` |
648
+ | `IntegrationEventSubscriber` | the event bus | `eventSubscriber` |
649
+ | `EventSubscribers` | the single `IntegrationEventSubscriber`, for every context | `eventSubscribers` |
650
+ | `ProcessedEventStore` | `InMemoryProcessedEventStore` | `processedEvents` |
651
+ | `ExecutionScope` | `AsyncLocalStorageExecutionScope` | `executionScope` |
652
+ | `ActorContext` | `ScopedActorContext`: the actor of the execution scope | `actorContext` |
653
+ | `DomainEventDispatcher` | `InMemoryDomainEventDispatcher`, which hears what the save of every repository announced; a failure of an `after-commit` handler goes to the `Logger` and the `ErrorReporter` (`reportAfterCommitFailures`) | `domainEvents` |
654
+ | `Logger` | `NoopLogger`. `app.stop()` flushes it last, after the error reporter, and stops all the same when the flush fails | `logger` |
655
+ | `Metrics` | `NoopMetrics` | `metrics` |
656
+ | `ErrorReporter` | `NoopErrorReporter`. The default buses and the default scheduler hand it their unexpected failures (never a `DomainError`), and `app.stop()` gives it two seconds to flush | `errorReporter` |
657
+ | `Cache` | `InMemoryCache` on `Clock` | `cache` |
658
+ | `Lock` | `InMemoryLock` on `Clock`: this process only | `lock` |
659
+ | `RateLimiter` | `InMemoryRateLimiter` on `Clock`: this process only | `rateLimiter` |
660
+ | `JobScheduler` | `InProcessJobScheduler` of `@ddd-ts-kit/scheduling` on `Clock`, `Lock`, `ExecutionScope`, `IdGenerator`, `Logger`, `Metrics` and `ErrorReporter` | `jobScheduler` |
661
+
662
+ The ports are the abstract classes of `@ddd-ts-kit/kernel`: a handler names `Clock` in its
663
+ dependency list without importing this package.
664
+
665
+ An option is an instance or a `(resolve) => instance` factory. An adapter with dependencies of
666
+ its own reads better in the module that owns them:
667
+
668
+ ```ts
669
+ export const persistenceModule = defineContextModule({
670
+ name: 'persistence',
671
+ register(builder) {
672
+ builder.bind(Database).toFactory(() => ({ client: connect(config.databaseUrl) }));
673
+ builder.rebind(UnitOfWork).toClass(DrizzleUnitOfWork, [Database]);
674
+ builder.rebind(IntegrationEventPublisher).toClass(OutboxPublisher, [OutboxStore]).as('publisher');
675
+ },
676
+ });
677
+ ```
678
+
679
+ Use cases publish through `IntegrationEventPublisher` and never through `EventBus`, so moving
680
+ to an outbox touches no context. A bus given for `CommandBus` or `QueryBus` needs a
681
+ `register(type, handler)` method for the declared handlers to be attached to it.
682
+
683
+ ## Scheduled jobs
684
+
685
+ A class of `application/scheduling/` that carries `@Scheduled` is a job of its context: nothing
686
+ else registers it.
687
+
688
+ ```ts
689
+ // contexts/billing/application/scheduling/send-reminders.job.ts
690
+ import { CommandBus, systemActor } from '@ddd-ts-kit/kernel';
691
+ import { Scheduled, type JobExecution, type ScheduledJob } from '@ddd-ts-kit/kernel/declarations';
692
+
693
+ @Scheduled('billing.send-reminders', { cron: '0 7 * * 1-5', timeZone: 'Europe/Paris' }, [CommandBus])
694
+ export class SendRemindersJob implements ScheduledJob {
695
+ constructor(private readonly commands: CommandBus) {}
696
+
697
+ async run(execution: JobExecution): Promise<void> {
698
+ await this.commands.execute(new SendReminders(systemActor('billing.send-reminders'), execution.scheduledAt));
699
+ }
700
+ }
701
+ ```
702
+
703
+ - **Registered when the application is created**, on `JobScheduler`, with the defaults of
704
+ its options filled in. The class is built on its first run, so a job that cannot be built
705
+ fails that run and nothing else.
706
+ - **Started by `start()`**, after the modules and once the consumers are subscribed: a job
707
+ sends commands whose events must find their handlers. **Stopped first by `stop()`**, which
708
+ waits for the runs in progress, then aborts them, while everything they use is still up.
709
+ - **Each run happens in `ExecutionScope`**, in a context of origin `schedule`, as
710
+ `systemActor('<job name>')`, with a correlation of its own. The scheduler does it, not the
711
+ job.
712
+ - **The default scheduler** is the in-process one of `@ddd-ts-kit/scheduling`, on the bound
713
+ `Clock` and `Lock`. With the default lock, which excludes inside one process only, several
714
+ instances each run every job: bind a lock between processes (`RedisLock`) before scaling
715
+ out. What one instance among several guarantees, and what it does not, is in that README.
716
+ - **An application without job** never resolves the scheduler.
717
+
718
+ `createApplication({ jobs })` says whether this process runs the jobs and what configuration
719
+ changes of each:
720
+
721
+ ```ts
722
+ createApplication({
723
+ modules,
724
+ jobs: {
725
+ start: config.runJobs, // default true
726
+ settings: {
727
+ 'billing.send-reminders': { enabled: config.reminders.enabled },
728
+ 'billing.export-ledger': { schedule: { every: '6h' }, timeoutMs: 300_000 },
729
+ },
730
+ },
731
+ });
732
+ ```
733
+
734
+ | Option | |
735
+ |---|---|
736
+ | `start: false` | the jobs are registered and described, `runNow` runs one, and `start()` never starts the scheduler: the process that serves requests next to a worker process that runs the jobs |
737
+ | `settings['<job name>']` | `enabled`, `schedule` and any option of the declaration (`overlap`, `missed`, `timeoutMs`, `jitterMs`, `singleInstance`), which win over what the class wrote. A key left undefined changes nothing |
738
+
739
+ `app.resolve(JobScheduler)` is the port: `runNow(name)` for an operations endpoint,
740
+ `describe()` for the next run and the last outcome of each job. A context extracted as its own
741
+ service takes its jobs with it: they belong to its module, and nothing is global.
742
+
743
+ ## Tests
744
+
745
+ A declared context is tested like an explicit one: the module is the unit, the fakes replace
746
+ its adapters.
747
+
748
+ ```ts
749
+ const app = createTestApplication({
750
+ modules: [contextModule('billing', billing)],
751
+ fakes: [fake(PaymentGateway, new InMemoryPayments())],
752
+ });
753
+ ```
754
+
755
+ `fake(Port, value)` is checked: the value implements the port. An in-memory class, an object
756
+ literal and a class written in the spec all fit, since a port is a shape.
757
+
758
+ `createTestApplication({ modules, fakes, now?, idPrefix?, actor?, session?, executionContext?, jobs? })` puts the modules on a deterministic
759
+ kernel, whether or not they list `kernelModule()`, and returns the application plus what a
760
+ spec touches:
761
+
762
+ | Member | Is |
763
+ |---|---|
764
+ | `commandBus`, `queryBus` | the in-memory buses, with every declared handler |
765
+ | `eventBus` | `InMemoryEventBus`; `eventBus.published` lists what the use cases announced. Every context listens on it, whatever `EventSubscribers` the modules bind |
766
+ | `domainEvents` | `InMemoryDomainEventDispatcher`: the save of every repository, adapter or fake, announces to it, with the timing of the application (at the end of the command, then after it for `after-commit` handlers); a spec may dispatch a domain event itself to see the declared handlers react, and `domainEvents.afterCommitFailures` holds what `after-commit` handlers threw |
767
+ | `clock` | `FixedClock` at `now` (default `2026-01-01T00:00:00.000Z`); `clock.advance(ms)` |
768
+ | `ids` | `SequentialIdGenerator`: `id-1`, `id-2`... |
769
+ | `actorContext` | `FixedActorContext`; `actAs(actor)` changes who infrastructure sees |
770
+ | `executionScope` | `FixedExecutionScope` (correlation `test-correlation`, origin `test`), following `actorContext` |
771
+ | `logger`, `metrics` | `InMemoryLogger` and `InMemoryMetrics`: assert `logger.entries`, `metrics.samples` |
772
+ | `errors` | `InMemoryErrorReporter`: `errors.reports` lists the unexpected failures of the commands, queries and jobs, as data |
773
+ | `cache` | `InMemoryCache` on `clock` |
774
+ | `lock`, `rateLimiter` | `InMemoryLock` and `InMemoryRateLimiter` on `clock` |
775
+ | `jobs` | `runNow(name)` runs a declared job as the scheduler would and throws what it threw; `scheduler` is a `ManualJobScheduler` on `clock`, not started |
776
+ | `get(Port)` | `resolve`, typed by the port |
777
+ | `as(actorOrSession, work)` | runs a block as someone else, then comes back |
778
+
779
+ `actor`, `session` and `executionContext` say who infrastructure sees by default: an actor, a
780
+ whole session (its actor becomes the current actor), or the complete execution context for a
781
+ spec about a tenant, a correlation or a deadline. `app.as` switches for one block and hands the
782
+ actor to it, which is what a spec about authorization wants to read:
783
+
784
+ ```ts
785
+ const app = createTestApplication({ modules: [billingModule, authorization], actor: accountant });
786
+
787
+ await app.as(accountant, (actor) => app.commandBus.execute(new IssueInvoice(actor, 'customer-1')));
788
+ await expect(
789
+ app.as(clerk, (actor) => app.commandBus.execute(new VoidInvoice(actor, 'inv-1'))),
790
+ ).rejects.toMatchObject({ code: 'FORBIDDEN' });
791
+
792
+ app.as(aSession().of(accountant).impersonatedBy('support-7').build(), () => {
793
+ // infrastructure sees the session: audit records the impersonation
794
+ });
795
+ ```
796
+
797
+ Use cases still receive the actor in the command or query; `as` only decides what the
798
+ repositories, the audit columns and the tenant scoping see while the block runs.
799
+
800
+ **Jobs are never started by time** in a test application: `start()` subscribes the consumers
801
+ and leaves the scheduler alone, whatever the clock does. A spec runs a job by name and asserts
802
+ the fakes, as it would for a command:
803
+
804
+ ```ts
805
+ await app.jobs.runNow('billing.send-reminders');
806
+
807
+ expect(mailer.sent).toHaveLength(2);
808
+ ```
809
+
810
+ For a spec about the schedule itself, `await app.jobs.scheduler.start()` then
811
+ `await app.jobs.scheduler.advance(ms)` move `app.clock` and run what becomes due, and
812
+ `app.jobs.scheduler.runs` lists what happened. The `jobs` option takes per-job settings, as
813
+ `createApplication({ jobs: { settings } })` does. Runs take their correlation ids from a
814
+ generator of their own (`job-run-1`...), so a job leaves the identifiers a spec predicts alone.
815
+
816
+ A fake must stand for a port that a module binds or that a class depends on, otherwise
817
+ `UNKNOWN_FAKE` is raised: a fake for the wrong port would silently leave the real adapter in
818
+ place. Consumers receive events once `await app.start()` ran.
819
+
820
+ `createApplication({ overrides: [override(Port, value)] })` is the same pair without that
821
+ check, and `overrideForTests(app, Port, value)` swaps a binding on an existing application;
822
+ call it before the first `resolve`, since a singleton already built keeps what it was given. An
823
+ override replaces the binding, may stand in for a module that is not loaded, and is not
824
+ decorated. Every application owns a tsyringe child container (`app.container`), so two tests
825
+ never share instances.
826
+
827
+ ## Checks
828
+
829
+ `createApplication` throws a `WiringError` (with a `code`) before anything is instantiated:
830
+
831
+ - `DUPLICATE_BINDING`: a port bound twice, or a class given to two statements or listed in two
832
+ modules, both modules named;
833
+ - `NOTHING_TO_REBIND`: a `rebind` of a port that no earlier module binds;
834
+ - `INVALID_PORT`: a class bound to a port that carries a body, or that it extends;
835
+ - `UNBOUND_DEPENDENCY`: a class dependency that no module binds, or handlers, consumers and
836
+ jobs declared without the bus, the subscriber or the scheduler they go on;
837
+ - `DEPENDENCY_CYCLE`: a cycle between class bindings, the path printed;
838
+ - `INCOMPLETE_BINDING`: a `bind`, `command`, `query`, `on` or `job` left without its target;
839
+ - `DUPLICATE_HANDLER`: a command or a query with two handlers, both modules named;
840
+ - `DUPLICATE_CONSUMER`: two consumers of the same event sharing a name;
841
+ - `DUPLICATE_PROJECTION`: two projections sharing a name;
842
+ - `BUS_WITHOUT_REGISTER`: handlers declared for a bus that cannot receive them;
843
+ - `UNKNOWN_FAKE`: see above;
844
+ - `UNDECLARED_CLASS`: a class listed in `contextModule` or `include` without a declaration;
845
+ - `DUPLICATE_JOB`: two jobs sharing a name, both modules named: the name is the lock, the
846
+ actor and the label of a job;
847
+ - `UNKNOWN_JOB`: settings given for a job that no module declares;
848
+ - `INVALID_JOB_SETTINGS`: a schedule or an option of the settings that cannot be read (a cron,
849
+ a time zone, a zero interval), or a job of an explicit module whose name or schedule cannot.
850
+
851
+ `DUPLICATE_HANDLER`, `DUPLICATE_CONSUMER` and `DUPLICATE_JOB` are reported before the
852
+ `DUPLICATE_BINDING` of the class they share, so the message says what the mistake is.
853
+
854
+ The checks do not tell the two styles apart. For a declared class the message names the class
855
+ and, when discovery loaded it, the file:
856
+
857
+ ```
858
+ Token "InvoiceRepository" is bound twice: by module "billing" (declared by class
859
+ KnexInvoiceRepository in /app/dist/contexts/billing/infrastructure/persistence/knex-invoice.repository.js)
860
+ and by module "invoicing" (declared by class InMemoryInvoiceRepository).
861
+ ```
862
+
863
+ Factories do not declare what they resolve, so a cycle or a missing port going through a
864
+ factory is reported when it is resolved (`DEPENDENCY_CYCLE`, `UNBOUND_TOKEN`).
865
+
866
+ ## Diagnostics
867
+
868
+ `app.describe()` returns the wiring as plain, serializable data: ports and classes by name.
869
+
870
+ ```ts
871
+ {
872
+ modules: ['kernel', 'ordering'],
873
+ bindings: [{ token: 'PlaceOrderHandler', module: 'ordering', kind: 'use-case', lifetime: 'singleton',
874
+ source: 'class', implementation: 'PlaceOrderHandler',
875
+ dependencies: ['OrderRepository', 'Clock'], overridden: false }],
876
+ commands: [{ message: 'PlaceOrder', handler: 'PlaceOrderHandler', module: 'ordering' }],
877
+ queries: [{ message: 'GetOrder', handler: 'GetOrderHandler', module: 'ordering' }],
878
+ integrationEventHandlers: [{ event: 'billing.payment.captured', version: 1, consumer: 'ordering.mark-order-paid',
879
+ handler: 'MarkOrderPaidOnPaymentCaptured', module: 'ordering' }],
880
+ domainEventHandlers: [],
881
+ policies: [],
882
+ jobs: [{ name: 'ordering.expire-carts', schedule: 'every 15m', overlap: 'skip', missed: 'skip', jitterMs: 0,
883
+ enabled: true, singleInstance: true, job: 'ExpireCartsJob', module: 'ordering' }],
884
+ projections: [{ name: 'ordering.order-list', projection: 'OrderListProjection', module: 'ordering',
885
+ applies: [{ kind: 'integration', event: 'billing.payment.captured', version: 1, method: 'paid' }] }],
886
+ overrides: [],
887
+ }
888
+ ```
889
+
890
+ An entry that comes from a declaration also carries `origin: 'declared'`, `declaredBy` (the
891
+ class) and, when discovery loaded it, `file`. An entry written in a module is `explicit` and
892
+ carries none of the three:
893
+
894
+ ```ts
895
+ { token: 'InvoiceRepository', module: 'billing', kind: 'repository', lifetime: 'singleton',
896
+ source: 'class', implementation: 'KnexInvoiceRepository', dependencies: ['Database'],
897
+ overridden: false, origin: 'declared', declaredBy: 'KnexInvoiceRepository',
898
+ file: '/app/dist/contexts/billing/infrastructure/persistence/knex-invoice.repository.js' }
899
+ ```
900
+
901
+ `domainEventHandlers` lists the declared handlers of domain events, publishers included, each
902
+ with its `phase` (`before-commit` or `after-commit`),
903
+ `policies` the declared policy classes, `jobs` the jobs with the settings they run with, those
904
+ of the application applied. What a job did last and when it runs next is not wiring: ask
905
+ `app.resolve(JobScheduler).describe()`.
906
+
907
+ ## Domain events and the unit of work
908
+
909
+ An effect of a domain event exists only if the state change that recorded it is committed.
910
+
911
+ A handler writes `await this.orders.save(order); return order.id;` and nothing else: no
912
+ `pullEvents()`, no `DomainEventDispatcher` in a use case. `kernelModule()` puts
913
+ `dispatchingOnSave` of the kernel around every binding of kind `repository`, in every module:
914
+ declared with `@Repository`, bound `.as('repository')`, or bound under a port that extends the
915
+ kernel `Repository` (which is a repository whether the module says so or not). A repository
916
+ adapter contains no dispatch code. The save only hands the events over; the unit of work of the
917
+ command dispatches them at its end.
918
+
919
+ | Question | Answer |
920
+ |---|---|
921
+ | Which methods | Every method of the repository that is handed an aggregate, or an array of them, and resolves: `save(order)`, `saveAll([a, b])`, `remove(order)`. A method that is handed no aggregate (`findById`, `remove(id)`) announces nothing: a removal that announces an event takes the aggregate |
922
+ | Which events, in which order | Those the aggregates of the call recorded, pulled once the call resolved: in the order of the arguments, then of recording, and across the saves of the command in the order of the saves |
923
+ | When | Once the command handler has returned, inside the transaction, before the commit: the default command bus runs the handler in `UnitOfWork.run`, and a save only queues. Every handler sees every save of the command. What the domain event handlers write, the outbox included, commits or rolls back with the state |
924
+ | After the commit | Handlers declared `phase: 'after-commit'` run once the transaction has committed, never when it rolled back, in the order the events were dispatched, awaited before the command answers. At most once: a crash between the commit and the run loses them |
925
+ | A `before-commit` handler fails | The command fails with what it threw and the unit of work rolls back. The events still waiting are dropped, no `after-commit` handler runs, and the aggregate instances have given their events: do not save them again, a retry reloads them |
926
+ | An `after-commit` handler fails | The command still succeeds. The failure is logged (`Logger.error`, "A domain event handler failed after the commit", with `domainEvent`, `errorType`, `errorCode`) and handed to the `ErrorReporter` under the operation `after-commit <event name>`; the other handlers still run |
927
+ | The save fails | Nothing is pulled: the events stay on the aggregate. The command fails, and the events queued by earlier saves are dropped |
928
+ | The command handler throws after a save, or the commit fails | The queued events are dropped: no handler of either phase for a handler that threw, no `after-commit` handler for a commit that failed; nothing stored, no outbox row |
929
+ | A handler saves another aggregate | Before the commit: its events wait behind the ones already queued and are dispatched in recording order, without recursion. After the commit: refused before the write with `SaveAfterCommitError`; send a command through the `CommandBus` instead |
930
+ | A handler sends a command | Before the commit, that command joins the unit of work in progress: one dispatch, at the end of the outermost, one commit. After the commit, it is a unit of work of its own |
931
+ | The same aggregate saved twice | The second save announces what was recorded since the first, which may be nothing |
932
+ | A save outside of any unit of work | Dispatches before it resolves, `before-commit` handlers then `after-commit` ones. Nothing is atomic then: the state is written, the handlers run, and a handler that fails rejects the save without undoing it |
933
+ | An adapter that pulls the events itself | Leaves nothing to dispatch: no double dispatch, and no dispatch at all unless the adapter does it. An adapter never calls `pullEvents()` |
934
+ | Order of the wrappers | Outermost first: the `decorate` hook, the instrumentation (the span of the save), the hand-over of the events, the aspects other modules put around the kind (`builder.around`), the aspects of the binding (`.with(retry(...))`). A retried save announces once |
935
+
936
+ A `before-commit` handler may only have effects the transaction covers, because the commit can
937
+ still fail after it ran: `domain-events/transactional-before-commit` holds it. An effect outside
938
+ the database is an `after-commit` handler when it may be lost, an integration event through the
939
+ outbox when it must not:
940
+
941
+ ```ts
942
+ @DomainEventHandler(InvoiceIssued, { name: 'billing.invoice.issued', phase: 'after-commit' }, [Cache])
943
+ export class InvoiceIssuedEventHandler implements DomainEventHandler<InvoiceIssued> {
944
+ constructor(private readonly cache: Cache) {}
945
+
946
+ async handle(event: InvoiceIssued): Promise<void> {
947
+ await this.cache.delete(cacheKey<InvoiceSummary>('invoice-summary', event.aggregateId));
948
+ }
949
+ }
950
+ ```
951
+
952
+ A `UnitOfWork` given through `kernelModule({ unitOfWork })` gets this timing for commands
953
+ whatever it is made of: the default command bus puts `withinUnitOfWork` of the kernel around
954
+ it. Built on `withinUnitOfWork` itself, as the ORM packages are, it gives the same timing to a
955
+ `run` called directly, the one of a consumer included.
956
+
957
+ In a test application, the adapter of a module and the fake given for a repository port both
958
+ announce to `app.domainEvents`, whether `kernelModule()` is listed or not. Without a container,
959
+ `dispatchingOnSave(repository, () => dispatcher)` of the kernel is the same wrapper, and
960
+ `createTestBuses().dispatching(fake)` of `@ddd-ts-kit/testing` applies it to a fake.
961
+
962
+ A module puts an aspect of its own around every binding of some kinds with
963
+ `builder.around(kinds, (resolve) => aspect)`: outside the aspects of each binding, inside the
964
+ `decorate` hook, the first declared outermost.
965
+
966
+ ## Observing the application
967
+
968
+ `kernelModule({ instrumentation })` takes what observes the application from outside: an object
969
+ whose members are wrappers, each optional, applied where the thing is built.
970
+
971
+ | Member | Wraps |
972
+ |---|---|
973
+ | `binding` | every instance of a binding declared with a kind, outside the dispatch of domain events |
974
+ | `useCase` | a bus middleware, on both default buses, after the use case guard and outside the unit of work; it reads `info.context`, the module of the handler |
975
+ | `publisher` | the `IntegrationEventPublisher` |
976
+ | `consumer` | what each declared consumer is subscribed with, told its consumer name, its module and its event type |
977
+ | `domainEvents` | the `DomainEventDispatcher` |
978
+ | `job` | each run of a declared job, inside its execution scope, told the job name and its module |
979
+ | `errorReporter` | the `ErrorReporter` |
980
+ | `executionScope` | told the `ExecutionScope` once it is built |
981
+
982
+ `traceApplication()` of `@ddd-ts-kit/tracing` is such an object, and makes one readable trace
983
+ per action; this package depends on no tracing library. A port given through an option of
984
+ `kernelModule` is wrapped too; one rebound by a later module is not.
985
+
986
+ ## The `decorate` hook
987
+
988
+ `createApplication({ decorate })` receives every instance a binding creates, handlers and
989
+ consumers included, with `{ token, context, kind? }` where `token` is the port class, and
990
+ returns the instance to hand out. It is the place for cross-cutting wrappers of a project.
991
+ Tracing comes through `kernelModule({ instrumentation })` instead, see "Observing the
992
+ application".
993
+
994
+ One binding takes its own wrappers with `.with(...)`, the first outermost:
995
+
996
+ ```ts
997
+ builder.bind(PaymentGateway).toClass(HttpPaymentGateway, [PaymentSettings]).as('external').with(retry({ attempts: 3 }), timeout(2000));
998
+ ```
999
+
1000
+ A `BindingAspect` is `(instance, { kind?, className, context, token }) => instance`, where
1001
+ `token` is the name of the port; the aspects of `@ddd-ts-kit/aspects` fit as they are, and its
1002
+ `aspects(...)` builds a `decorate` hook. The `decorate` hook comes around the aspects of a
1003
+ binding.
1004
+
1005
+ ## The tsyringe container
1006
+
1007
+ `app.container` is the tsyringe child container of the application, for interoperability. A
1008
+ port is registered there under `tokenIdentity(port)`, a symbol, never under the class: tsyringe
1009
+ would try to build a class it is asked for, and an abstract class is not one of its tokens.
1010
+
1011
+ ```ts
1012
+ import { tokenIdentity } from '@ddd-ts-kit/di';
1013
+
1014
+ app.container.isRegistered(tokenIdentity(Clock)); // true
1015
+ app.container.resolve<Clock>(tokenIdentity(Clock)); // what app.resolve(Clock) answers
1016
+ app.container.isRegistered(PlaceOrderHandler); // false: bound under its identity too
1017
+ ```
1018
+
1019
+ `app.resolve(Port)` is the typed way in, and the one that reports a cycle or an unbound port
1020
+ with the names of who asked.
1021
+
1022
+ ## Access control
1023
+
1024
+ A use case says what it requires, or says it is public: on the message itself with
1025
+ `@Requires(permission)` or `@Public()`, or where it is registered:
1026
+
1027
+ ```ts
1028
+ builder.command(IssueInvoice).requires(permissions['invoice.issue']).handledBy(IssueInvoiceHandler, [InvoiceRepository, Clock]);
1029
+ builder.query(GetCatalog).public().handledBy(GetCatalogHandler, [Catalog]);
1030
+ ```
1031
+
1032
+ `.requires` exists only for messages that carry an actor (`AuthorizedCommand`,
1033
+ `AuthorizedQuery` of the kernel). With `createApplication({ modules, authorization: 'required' })`
1034
+ an application whose command or query declares neither, in either way, does not start
1035
+ (`UNDECLARED_AUTHORIZATION`), and one that declares them without a module enforcing them does
1036
+ not either (`AUTHORIZATION_NOT_ENFORCED`).
1037
+
1038
+ Three classes of this package carry it:
1039
+
1040
+ | Class | Is |
1041
+ |---|---|
1042
+ | `UseCaseGuard` | the port both buses ask before anything else, the unit of work included: `check(message, next, info)`. Unbound, every message passes; an authorization module binds it |
1043
+ | `UseCaseDeclarations` | a value, `{ list }`: what every command and query declared, for whoever enforces it |
1044
+ | `DeclaredPolicies` | a value, `{ list }`: the `@Policy` classes of the contexts, each resolved by its class |
1045
+
1046
+ The enforcement itself and the authorizers live in `@ddd-ts-kit/auth` (`authorizationModule`
1047
+ from `@ddd-ts-kit/auth/di`). `ActorContext` is bound by `kernelModule()`; the test application
1048
+ takes an `actor` option, and lets every message through unless a listed module binds
1049
+ `UseCaseGuard`.
1050
+
1051
+ ## Limits
1052
+
1053
+ - A class plays one role: one declaration per class, and an adapter that implements two ports
1054
+ is bound explicitly, once per port.
1055
+ - A class is one binding: a handler class serves one statement, a declared class one module.
1056
+ - A class decorator of another library that replaces the class (answers a new one) must be
1057
+ written below the declaration, so that the declaration lands on the class that is exported.
1058
+ A decorator that only annotates, as `@Injectable()` does, composes in either order.
1059
+ - A declaration is not inherited: a subclass declares itself or is bound explicitly.
1060
+ - A consumer name, a policy name, a job name and the name of a domain event are written in the
1061
+ declaration; they are not derived from the class, which a refactoring renames.
1062
+ - A port of your own is identified by its class: see "Identity" for a port loaded twice.
1063
+ - Discovered files are imported by nothing. In a project using discovery, tell knip that they
1064
+ are entries and dependency-cruiser that they are not orphans:
1065
+
1066
+ ```jsonc
1067
+ // knip.json
1068
+ { "entry": ["src/main.ts", "src/contexts/*/{application,infrastructure}/**/*.{command,query,handler,integration-handler,publisher,event-handler,policy,job,projection,repository,gateway,adapter}.ts"] }
1069
+ ```
1070
+
1071
+ ```js
1072
+ // .dependency-cruiser.cjs, in the pathNot of the no-orphans rule
1073
+ '\\.(command|query|handler|integration-handler|publisher|event-handler|policy|job|projection|repository|gateway|adapter)\\.ts$'
1074
+ ```
1075
+
1076
+ With a barrel and `contextModule` neither setting is needed.