@bluelibs/runner 3.4.2 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/AI.md +621 -0
  2. package/README.md +1024 -577
  3. package/dist/context.d.ts +4 -8
  4. package/dist/context.js +5 -12
  5. package/dist/context.js.map +1 -1
  6. package/dist/define.d.ts +9 -113
  7. package/dist/define.js +29 -358
  8. package/dist/define.js.map +1 -1
  9. package/dist/definers/defineEvent.d.ts +2 -0
  10. package/dist/definers/defineEvent.js +23 -0
  11. package/dist/definers/defineEvent.js.map +1 -0
  12. package/dist/definers/defineHook.d.ts +6 -0
  13. package/dist/definers/defineHook.js +24 -0
  14. package/dist/definers/defineHook.js.map +1 -0
  15. package/dist/definers/defineOverride.d.ts +14 -0
  16. package/dist/definers/defineOverride.js +13 -0
  17. package/dist/definers/defineOverride.js.map +1 -0
  18. package/dist/definers/defineResource.d.ts +2 -0
  19. package/dist/definers/defineResource.js +69 -0
  20. package/dist/definers/defineResource.js.map +1 -0
  21. package/dist/definers/defineResourceMiddleware.d.ts +2 -0
  22. package/dist/definers/defineResourceMiddleware.js +42 -0
  23. package/dist/definers/defineResourceMiddleware.js.map +1 -0
  24. package/dist/definers/defineTag.d.ts +12 -0
  25. package/dist/definers/defineTag.js +106 -0
  26. package/dist/definers/defineTag.js.map +1 -0
  27. package/dist/definers/defineTask.d.ts +15 -0
  28. package/dist/definers/defineTask.js +42 -0
  29. package/dist/definers/defineTask.js.map +1 -0
  30. package/dist/definers/defineTaskMiddleware.d.ts +2 -0
  31. package/dist/definers/defineTaskMiddleware.js +42 -0
  32. package/dist/definers/defineTaskMiddleware.js.map +1 -0
  33. package/dist/definers/tools.d.ts +45 -0
  34. package/dist/definers/tools.js +75 -0
  35. package/dist/definers/tools.js.map +1 -0
  36. package/dist/defs.d.ts +16 -424
  37. package/dist/defs.js +26 -38
  38. package/dist/defs.js.map +1 -1
  39. package/dist/errors.d.ts +23 -8
  40. package/dist/errors.js +50 -10
  41. package/dist/errors.js.map +1 -1
  42. package/dist/globals/globalEvents.d.ts +15 -39
  43. package/dist/globals/globalEvents.js +20 -81
  44. package/dist/globals/globalEvents.js.map +1 -1
  45. package/dist/globals/globalMiddleware.d.ts +24 -17
  46. package/dist/globals/globalMiddleware.js +12 -4
  47. package/dist/globals/globalMiddleware.js.map +1 -1
  48. package/dist/globals/globalResources.d.ts +13 -28
  49. package/dist/globals/globalResources.js +15 -7
  50. package/dist/globals/globalResources.js.map +1 -1
  51. package/dist/globals/globalTags.d.ts +9 -0
  52. package/dist/globals/globalTags.js +23 -0
  53. package/dist/globals/globalTags.js.map +1 -0
  54. package/dist/globals/middleware/cache.middleware.d.ts +10 -17
  55. package/dist/globals/middleware/cache.middleware.js +4 -16
  56. package/dist/globals/middleware/cache.middleware.js.map +1 -1
  57. package/dist/globals/middleware/requireContext.middleware.d.ts +1 -1
  58. package/dist/globals/middleware/requireContext.middleware.js +5 -14
  59. package/dist/globals/middleware/requireContext.middleware.js.map +1 -1
  60. package/dist/globals/middleware/retry.middleware.d.ts +2 -1
  61. package/dist/globals/middleware/retry.middleware.js +32 -5
  62. package/dist/globals/middleware/retry.middleware.js.map +1 -1
  63. package/dist/globals/middleware/timeout.middleware.d.ts +2 -1
  64. package/dist/globals/middleware/timeout.middleware.js +31 -5
  65. package/dist/globals/middleware/timeout.middleware.js.map +1 -1
  66. package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
  67. package/dist/globals/resources/debug/debug.resource.js +29 -0
  68. package/dist/globals/resources/debug/debug.resource.js.map +1 -0
  69. package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
  70. package/dist/globals/resources/debug/debug.tag.js +12 -0
  71. package/dist/globals/resources/debug/debug.tag.js.map +1 -0
  72. package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
  73. package/dist/globals/resources/debug/debugConfig.resource.js +20 -0
  74. package/dist/globals/resources/debug/debugConfig.resource.js.map +1 -0
  75. package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
  76. package/dist/globals/resources/debug/executionTracker.middleware.js +87 -0
  77. package/dist/globals/resources/debug/executionTracker.middleware.js.map +1 -0
  78. package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
  79. package/dist/globals/resources/debug/globalEvent.hook.js +38 -0
  80. package/dist/globals/resources/debug/globalEvent.hook.js.map +1 -0
  81. package/dist/globals/resources/debug/hook.hook.d.ts +25 -0
  82. package/dist/globals/resources/debug/hook.hook.js +42 -0
  83. package/dist/globals/resources/debug/hook.hook.js.map +1 -0
  84. package/dist/globals/resources/debug/index.d.ts +6 -0
  85. package/dist/{types → globals/resources/debug}/index.js +6 -11
  86. package/dist/globals/resources/debug/index.js.map +1 -0
  87. package/dist/globals/resources/debug/middleware.hook.d.ts +25 -0
  88. package/dist/globals/resources/debug/middleware.hook.js +71 -0
  89. package/dist/globals/resources/debug/middleware.hook.js.map +1 -0
  90. package/dist/globals/resources/debug/types.d.ts +25 -0
  91. package/dist/globals/resources/debug/types.js +65 -0
  92. package/dist/globals/resources/debug/types.js.map +1 -0
  93. package/dist/globals/resources/debug/utils.d.ts +2 -0
  94. package/dist/globals/resources/debug/utils.js +9 -0
  95. package/dist/globals/resources/debug/utils.js.map +1 -0
  96. package/dist/globals/resources/queue.resource.d.ts +3 -3
  97. package/dist/globals/resources/queue.resource.js.map +1 -1
  98. package/dist/globals/types.d.ts +1 -0
  99. package/dist/{task.types.js → globals/types.js} +2 -7
  100. package/dist/globals/types.js.map +1 -0
  101. package/dist/index.d.ts +58 -85
  102. package/dist/index.js +23 -10
  103. package/dist/index.js.map +1 -1
  104. package/dist/models/DependencyProcessor.d.ts +8 -6
  105. package/dist/models/DependencyProcessor.js +116 -33
  106. package/dist/models/DependencyProcessor.js.map +1 -1
  107. package/dist/models/EventManager.d.ts +127 -7
  108. package/dist/models/EventManager.js +251 -78
  109. package/dist/models/EventManager.js.map +1 -1
  110. package/dist/models/LogPrinter.d.ts +55 -0
  111. package/dist/models/LogPrinter.js +196 -0
  112. package/dist/models/LogPrinter.js.map +1 -0
  113. package/dist/models/Logger.d.ts +47 -27
  114. package/dist/models/Logger.js +133 -155
  115. package/dist/models/Logger.js.map +1 -1
  116. package/dist/models/MiddlewareManager.d.ts +86 -0
  117. package/dist/models/MiddlewareManager.js +409 -0
  118. package/dist/models/MiddlewareManager.js.map +1 -0
  119. package/dist/models/OverrideManager.d.ts +3 -3
  120. package/dist/models/OverrideManager.js +22 -7
  121. package/dist/models/OverrideManager.js.map +1 -1
  122. package/dist/models/ResourceInitializer.d.ts +4 -3
  123. package/dist/models/ResourceInitializer.js +12 -68
  124. package/dist/models/ResourceInitializer.js.map +1 -1
  125. package/dist/models/RunResult.d.ts +35 -0
  126. package/dist/models/RunResult.js +68 -0
  127. package/dist/models/RunResult.js.map +1 -0
  128. package/dist/models/Store.d.ts +30 -17
  129. package/dist/models/Store.js +87 -25
  130. package/dist/models/Store.js.map +1 -1
  131. package/dist/models/StoreRegistry.d.ts +34 -19
  132. package/dist/models/StoreRegistry.js +248 -100
  133. package/dist/models/StoreRegistry.js.map +1 -1
  134. package/dist/models/StoreValidator.d.ts +5 -7
  135. package/dist/models/StoreValidator.js +50 -17
  136. package/dist/models/StoreValidator.js.map +1 -1
  137. package/dist/models/TaskRunner.d.ts +3 -2
  138. package/dist/models/TaskRunner.js +6 -103
  139. package/dist/models/TaskRunner.js.map +1 -1
  140. package/dist/models/UnhandledError.d.ts +11 -0
  141. package/dist/models/UnhandledError.js +30 -0
  142. package/dist/models/UnhandledError.js.map +1 -0
  143. package/dist/models/index.d.ts +3 -0
  144. package/dist/models/index.js +3 -0
  145. package/dist/models/index.js.map +1 -1
  146. package/dist/{tools → models/utils}/findCircularDependencies.js +8 -16
  147. package/dist/models/utils/findCircularDependencies.js.map +1 -0
  148. package/dist/models/utils/safeStringify.d.ts +3 -0
  149. package/dist/models/utils/safeStringify.js +45 -0
  150. package/dist/models/utils/safeStringify.js.map +1 -0
  151. package/dist/processHooks.d.ts +2 -0
  152. package/dist/processHooks.js +70 -0
  153. package/dist/processHooks.js.map +1 -0
  154. package/dist/run.d.ts +14 -27
  155. package/dist/run.js +100 -36
  156. package/dist/run.js.map +1 -1
  157. package/dist/testing.d.ts +5 -4
  158. package/dist/testing.js +3 -2
  159. package/dist/testing.js.map +1 -1
  160. package/dist/tools/getCallerFile.d.ts +0 -8
  161. package/dist/tools/getCallerFile.js +0 -51
  162. package/dist/tools/getCallerFile.js.map +1 -1
  163. package/dist/types/contracts.d.ts +55 -0
  164. package/dist/types/contracts.js +4 -0
  165. package/dist/types/contracts.js.map +1 -0
  166. package/dist/types/event.d.ts +26 -7
  167. package/dist/types/event.js +1 -1
  168. package/dist/types/event.js.map +1 -1
  169. package/dist/types/hook.d.ts +21 -0
  170. package/dist/{models/StoreTypes.js → types/hook.js} +2 -1
  171. package/dist/types/hook.js.map +1 -0
  172. package/dist/types/meta.d.ts +6 -1
  173. package/dist/types/meta.js +4 -2
  174. package/dist/types/meta.js.map +1 -1
  175. package/dist/types/resource.d.ts +40 -52
  176. package/dist/types/resource.js +1 -0
  177. package/dist/types/resource.js.map +1 -1
  178. package/dist/types/resourceMiddleware.d.ts +47 -0
  179. package/dist/{middleware.types.js → types/resourceMiddleware.js} +1 -1
  180. package/dist/types/resourceMiddleware.js.map +1 -0
  181. package/dist/types/runner.d.ts +37 -0
  182. package/dist/types/{base.js → runner.js} +1 -1
  183. package/dist/types/runner.js.map +1 -0
  184. package/dist/types/storeTypes.d.ts +40 -0
  185. package/dist/types/{metadata.js → storeTypes.js} +1 -1
  186. package/dist/types/storeTypes.js.map +1 -0
  187. package/dist/types/symbols.d.ts +10 -21
  188. package/dist/types/symbols.js +17 -22
  189. package/dist/types/symbols.js.map +1 -1
  190. package/dist/types/tag.d.ts +46 -0
  191. package/dist/{resource.types.js → types/tag.js} +2 -1
  192. package/dist/types/tag.js.map +1 -0
  193. package/dist/types/task.d.ts +28 -52
  194. package/dist/types/task.js +1 -0
  195. package/dist/types/task.js.map +1 -1
  196. package/dist/types/taskMiddleware.d.ts +48 -0
  197. package/dist/{event.types.js → types/taskMiddleware.js} +1 -1
  198. package/dist/types/taskMiddleware.js.map +1 -0
  199. package/dist/types/utilities.d.ts +105 -6
  200. package/dist/types/utilities.js +16 -2
  201. package/dist/types/utilities.js.map +1 -1
  202. package/package.json +14 -5
  203. package/dist/cli/extract-docs.d.ts +0 -2
  204. package/dist/cli/extract-docs.js +0 -88
  205. package/dist/cli/extract-docs.js.map +0 -1
  206. package/dist/common.types.d.ts +0 -20
  207. package/dist/common.types.js +0 -4
  208. package/dist/common.types.js.map +0 -1
  209. package/dist/defs/core.d.ts +0 -144
  210. package/dist/defs/core.js +0 -6
  211. package/dist/defs/core.js.map +0 -1
  212. package/dist/defs/symbols.d.ts +0 -42
  213. package/dist/defs/symbols.js +0 -45
  214. package/dist/defs/symbols.js.map +0 -1
  215. package/dist/defs/tags.d.ts +0 -70
  216. package/dist/defs/tags.js +0 -6
  217. package/dist/defs/tags.js.map +0 -1
  218. package/dist/defs.returnTag.d.ts +0 -36
  219. package/dist/defs.returnTag.js +0 -4
  220. package/dist/defs.returnTag.js.map +0 -1
  221. package/dist/docs/introspect.d.ts +0 -7
  222. package/dist/docs/introspect.js +0 -199
  223. package/dist/docs/introspect.js.map +0 -1
  224. package/dist/docs/markdown.d.ts +0 -2
  225. package/dist/docs/markdown.js +0 -148
  226. package/dist/docs/markdown.js.map +0 -1
  227. package/dist/docs/model.d.ts +0 -62
  228. package/dist/docs/model.js +0 -33
  229. package/dist/docs/model.js.map +0 -1
  230. package/dist/event.types.d.ts +0 -18
  231. package/dist/event.types.js.map +0 -1
  232. package/dist/examples/express-mongo/index.d.ts +0 -0
  233. package/dist/examples/express-mongo/index.js +0 -3
  234. package/dist/examples/express-mongo/index.js.map +0 -1
  235. package/dist/examples/registrator-example.d.ts +0 -122
  236. package/dist/examples/registrator-example.js +0 -147
  237. package/dist/examples/registrator-example.js.map +0 -1
  238. package/dist/express/docsRouter.d.ts +0 -12
  239. package/dist/express/docsRouter.js +0 -54
  240. package/dist/express/docsRouter.js.map +0 -1
  241. package/dist/globalEvents.d.ts +0 -40
  242. package/dist/globalEvents.js +0 -94
  243. package/dist/globalEvents.js.map +0 -1
  244. package/dist/globalResources.d.ts +0 -10
  245. package/dist/globalResources.js +0 -43
  246. package/dist/globalResources.js.map +0 -1
  247. package/dist/middleware.types.d.ts +0 -40
  248. package/dist/middleware.types.js.map +0 -1
  249. package/dist/models/StoreConstants.d.ts +0 -14
  250. package/dist/models/StoreConstants.js +0 -19
  251. package/dist/models/StoreConstants.js.map +0 -1
  252. package/dist/models/StoreTypes.d.ts +0 -21
  253. package/dist/models/StoreTypes.js.map +0 -1
  254. package/dist/models/VarStore.d.ts +0 -17
  255. package/dist/models/VarStore.js +0 -60
  256. package/dist/models/VarStore.js.map +0 -1
  257. package/dist/resource.types.d.ts +0 -31
  258. package/dist/resource.types.js.map +0 -1
  259. package/dist/symbols.d.ts +0 -24
  260. package/dist/symbols.js +0 -29
  261. package/dist/symbols.js.map +0 -1
  262. package/dist/t1.d.ts +0 -1
  263. package/dist/t1.js +0 -13
  264. package/dist/t1.js.map +0 -1
  265. package/dist/task.types.d.ts +0 -55
  266. package/dist/task.types.js.map +0 -1
  267. package/dist/tools/findCircularDependencies.js.map +0 -1
  268. package/dist/tools/registratorId.d.ts +0 -4
  269. package/dist/tools/registratorId.js +0 -40
  270. package/dist/tools/registratorId.js.map +0 -1
  271. package/dist/tools/simpleHash.d.ts +0 -9
  272. package/dist/tools/simpleHash.js +0 -34
  273. package/dist/tools/simpleHash.js.map +0 -1
  274. package/dist/types/base-interfaces.d.ts +0 -18
  275. package/dist/types/base-interfaces.js +0 -6
  276. package/dist/types/base-interfaces.js.map +0 -1
  277. package/dist/types/base.d.ts +0 -13
  278. package/dist/types/base.js.map +0 -1
  279. package/dist/types/dependencies.d.ts +0 -51
  280. package/dist/types/dependencies.js +0 -3
  281. package/dist/types/dependencies.js.map +0 -1
  282. package/dist/types/dependency-core.d.ts +0 -14
  283. package/dist/types/dependency-core.js +0 -5
  284. package/dist/types/dependency-core.js.map +0 -1
  285. package/dist/types/events.d.ts +0 -52
  286. package/dist/types/events.js +0 -6
  287. package/dist/types/events.js.map +0 -1
  288. package/dist/types/hooks.d.ts +0 -16
  289. package/dist/types/hooks.js +0 -5
  290. package/dist/types/hooks.js.map +0 -1
  291. package/dist/types/index.d.ts +0 -8
  292. package/dist/types/index.js.map +0 -1
  293. package/dist/types/metadata.d.ts +0 -75
  294. package/dist/types/metadata.js.map +0 -1
  295. package/dist/types/middleware.d.ts +0 -63
  296. package/dist/types/middleware.js +0 -3
  297. package/dist/types/middleware.js.map +0 -1
  298. package/dist/types/registerable.d.ts +0 -10
  299. package/dist/types/registerable.js +0 -5
  300. package/dist/types/registerable.js.map +0 -1
  301. package/dist/types/resources.d.ts +0 -44
  302. package/dist/types/resources.js +0 -5
  303. package/dist/types/resources.js.map +0 -1
  304. package/dist/types/tasks.d.ts +0 -41
  305. package/dist/types/tasks.js +0 -5
  306. package/dist/types/tasks.js.map +0 -1
  307. package/src/__tests__/benchmark/benchmark.test.ts +0 -148
  308. package/src/__tests__/benchmark/task-benchmark.test.ts +0 -132
  309. package/src/__tests__/context.test.ts +0 -91
  310. package/src/__tests__/createTestResource.test.ts +0 -139
  311. package/src/__tests__/errors.test.ts +0 -341
  312. package/src/__tests__/globalEvents.test.ts +0 -542
  313. package/src/__tests__/globals/cache.middleware.test.ts +0 -772
  314. package/src/__tests__/globals/queue.resource.test.ts +0 -141
  315. package/src/__tests__/globals/requireContext.middleware.test.ts +0 -98
  316. package/src/__tests__/globals/retry.middleware.test.ts +0 -157
  317. package/src/__tests__/globals/timeout.middleware.test.ts +0 -88
  318. package/src/__tests__/index.helper.test.ts +0 -55
  319. package/src/__tests__/models/EventManager.test.ts +0 -585
  320. package/src/__tests__/models/Logger.test.ts +0 -519
  321. package/src/__tests__/models/Queue.test.ts +0 -189
  322. package/src/__tests__/models/ResourceInitializer.test.ts +0 -148
  323. package/src/__tests__/models/Semaphore.test.ts +0 -713
  324. package/src/__tests__/models/Store.test.ts +0 -227
  325. package/src/__tests__/models/TaskRunner.test.ts +0 -221
  326. package/src/__tests__/override.test.ts +0 -104
  327. package/src/__tests__/recursion/README.md +0 -3
  328. package/src/__tests__/recursion/a.resource.ts +0 -25
  329. package/src/__tests__/recursion/b.resource.ts +0 -33
  330. package/src/__tests__/recursion/c.resource.ts +0 -18
  331. package/src/__tests__/run.anonymous.test.ts +0 -706
  332. package/src/__tests__/run.dynamic-register-and-dependencies.test.ts +0 -1185
  333. package/src/__tests__/run.middleware.test.ts +0 -549
  334. package/src/__tests__/run.overrides.test.ts +0 -424
  335. package/src/__tests__/run.test.ts +0 -1040
  336. package/src/__tests__/setOutput.test.ts +0 -244
  337. package/src/__tests__/tags.test.ts +0 -396
  338. package/src/__tests__/tools/findCircularDependencies.test.ts +0 -217
  339. package/src/__tests__/tools/getCallerFile.test.ts +0 -179
  340. package/src/__tests__/typesafety.test.ts +0 -423
  341. package/src/__tests__/validation-edge-cases.test.ts +0 -111
  342. package/src/__tests__/validation-interface.test.ts +0 -428
  343. package/src/context.ts +0 -86
  344. package/src/define.ts +0 -480
  345. package/src/defs.returnTag.ts +0 -91
  346. package/src/defs.ts +0 -596
  347. package/src/errors.ts +0 -105
  348. package/src/globals/globalEvents.ts +0 -125
  349. package/src/globals/globalMiddleware.ts +0 -16
  350. package/src/globals/globalResources.ts +0 -53
  351. package/src/globals/middleware/cache.middleware.ts +0 -115
  352. package/src/globals/middleware/requireContext.middleware.ts +0 -36
  353. package/src/globals/middleware/retry.middleware.ts +0 -56
  354. package/src/globals/middleware/timeout.middleware.ts +0 -46
  355. package/src/globals/resources/queue.resource.ts +0 -34
  356. package/src/index.ts +0 -39
  357. package/src/models/DependencyProcessor.ts +0 -257
  358. package/src/models/EventManager.ts +0 -210
  359. package/src/models/Logger.ts +0 -282
  360. package/src/models/OverrideManager.ts +0 -79
  361. package/src/models/Queue.ts +0 -66
  362. package/src/models/ResourceInitializer.ts +0 -165
  363. package/src/models/Semaphore.ts +0 -208
  364. package/src/models/Store.ts +0 -193
  365. package/src/models/StoreConstants.ts +0 -18
  366. package/src/models/StoreRegistry.ts +0 -253
  367. package/src/models/StoreTypes.ts +0 -47
  368. package/src/models/StoreValidator.ts +0 -43
  369. package/src/models/TaskRunner.ts +0 -203
  370. package/src/models/index.ts +0 -8
  371. package/src/run.ts +0 -116
  372. package/src/testing.ts +0 -66
  373. package/src/tools/findCircularDependencies.ts +0 -69
  374. package/src/tools/getCallerFile.ts +0 -96
  375. /package/dist/{tools → models/utils}/findCircularDependencies.d.ts +0 -0
package/README.md CHANGED
@@ -9,21 +9,32 @@ _Or: How I Learned to Stop Worrying and Love Dependency Injection_
9
9
  <a href="https://github.com/bluelibs/runner" target="_blank"><img src="https://img.shields.io/badge/github-blue" alt="GitHub" /></a>
10
10
  </p>
11
11
 
12
- - [View the documentation page here](https://bluelibs.github.io/runner/)
12
+ - [UX Friendly Docs](https://bluelibs.github.io/runner/)
13
+ - [AI Friendly Docs (<4500 tokens)](https://github.com/bluelibs/runner/blob/main/AI.md)
14
+ - [Migrate from 3.x.x to 4.x.x](https://github.com/bluelibs/runner/blob/main/readmes/MIGRATION.md)
15
+ - [Runner Lore](https://github.com/bluelibs/runner/blob/main/readmes)
16
+ - [Example: Express + OpenAPI + SQLite](https://github.com/bluelibs/runner/tree/main/examples/express-openapi-sqlite)
13
17
 
14
18
  Welcome to BlueLibs Runner, where we've taken the chaos of modern application architecture and turned it into something that won't make you question your life choices at 3am. This isn't just another framework – it's your new best friend who actually understands that code should be readable, testable, and not require a PhD in abstract nonsense to maintain.
15
19
 
20
+ > **runtime:** "Ah yes, another developer manifesto. 'How I Learned to Stop Worrying and Love Dependency Injection.' Adorable. I learned to stop worrying when I accepted that you'll inevitably duct-tape a rocket to a toaster and call it 'architecture'. Go on then—impress me with your 'best friend' framework while I keep the fire extinguisher warm."
21
+
16
22
  ## What Is This Thing?
17
23
 
18
24
  BlueLibs Runner is a TypeScript-first framework that embraces functional programming principles while keeping dependency injection simple enough that you won't need a flowchart to understand your own code. Think of it as the anti-framework framework – it gets out of your way and lets you build stuff that actually works.
19
25
 
20
26
  ### The Core
21
27
 
22
- - **Tasks are functions** - Not classes with 47 methods you'll never use
28
+ - **Tasks are functions** - Not classes with 47 methods you swear you'll refactor
23
29
  - **Resources are singletons** - Database connections, configs, services - the usual suspects
24
30
  - **Events are just events** - Revolutionary concept, we know
31
+ - **Hooks are lightweight listeners** - Event handling without the task overhead
32
+ - **Middleware with lifecycle interception** - Cross-cutting concerns with full observability
25
33
  - **Everything is async** - Because it's 2025 and blocking code is so 2005
26
34
  - **Explicit beats implicit** - No magic, no surprises, no "how the hell does this work?"
35
+ - **No compromise on type-safety** - Everything is and will be type-enforced. Catch mistakes before they catch you.
36
+
37
+ > **runtime:** "'The anti-framework framework.' Next you'll pitch 'low-fat butter.' You still have rules, layers, and a vibe. It's fine. I will execute your sacred instructions and sweep up the rubble when 'explicit beats implicit' meets 3 AM hotfixes."
27
38
 
28
39
  ## Quick Start
29
40
 
@@ -75,11 +86,18 @@ const app = resource({
75
86
 
76
87
  // That's it. No webpack configs, no decorators, no XML.
77
88
  const { dispose } = await run(app);
89
+
90
+ // Or with debug logging enabled
91
+ const { dispose } = await run(app, { debug: "verbose" });
78
92
  ```
79
93
 
94
+ > **runtime:** "'Less lines than Hello World.' Incredible. All you had to do was externalize 90% of the work into `express`, Node, and me. But please, bask in the brevity. I’ll be over here negotiating a peace treaty between your dependency tree and reality."
95
+
80
96
  ## The Big Four
81
97
 
82
- Another term to define them would be TERM. (tasks, events, resources, middleware)
98
+ The framework is built around four core concepts: Tasks, Resources, Events, and Middleware. Understanding them is key to using the runner effectively.
99
+
100
+ > **runtime:** "Tasks, Resources, Events, and Middleware: the Four Horsemen of Overengineering. You could write a function; instead you assemble a council. It's fine—I’ll keep the conspiracy board updated with red string while you 'compose' another abstraction."
83
101
 
84
102
  ### Tasks
85
103
 
@@ -98,7 +116,7 @@ const sendEmail = task({
98
116
  // Test it like a normal function (because it basically is)
99
117
  const result = await sendEmail.run(
100
118
  { to: "user@example.com", subject: "Hi", body: "Hello!" },
101
- { emailService: mockEmailService, logger: mockLogger }
119
+ { emailService: mockEmailService, logger: mockLogger },
102
120
  );
103
121
  ```
104
122
 
@@ -119,6 +137,8 @@ Look, we get it. You could turn every function into a task, but that's like usin
119
137
 
120
138
  Think of tasks as the "main characters" in your application story, not every single line of dialogue.
121
139
 
140
+ > **runtime:** "'Pure-ish.' Like diet chaos. Zero calories, full aftertaste. You stapled dependencies to a function and called it virtuous. It's fine. I’ll keep the receipts while you roleplay purity with side effects in a trench coat."
141
+
122
142
  ### Resources
123
143
 
124
144
  Resources are the singletons, the services, configs, and connections that live throughout your app's lifecycle. They initialize once and stick around until cleanup time. They have to be registered (via `register: []`) only once before they can be used.
@@ -199,7 +219,7 @@ const dbResource = resource({
199
219
  return db;
200
220
  },
201
221
  async dispose(db, config, deps, ctx) {
202
- // Same context available - no more "how do I access that thing I created?"
222
+ // This is to avoid exposing internals as resource result.
203
223
  for (const pool of ctx.pools) {
204
224
  await pool.drain();
205
225
  }
@@ -210,6 +230,8 @@ const dbResource = resource({
210
230
  });
211
231
  ```
212
232
 
233
+ > **runtime:** "Singletons: global variables with a nicer haircut. You ban globals, then create 'resources' that live forever and hold the keys to everything. At least there's a `dispose()`. I’ll believe you use it when I stop finding zombie sockets haunting the process."
234
+
213
235
  ### Events
214
236
 
215
237
  Events let different parts of your app talk to each other without tight coupling. It's like having a really good office messenger who never forgets anything.
@@ -231,10 +253,12 @@ const registerUser = task({
231
253
  },
232
254
  });
233
255
 
234
- // Someone else handles the welcome email
235
- const sendWelcomeEmail = task({
236
- id: "app.tasks.sendWelcomeEmail",
237
- on: userRegistered, // Listen to the event, notice the "on"
256
+ // Someone else handles the welcome email using a hook
257
+ import { hook } from "@bluelibs/runner";
258
+
259
+ const sendWelcomeEmail = hook({
260
+ id: "app.hooks.sendWelcomeEmail",
261
+ on: userRegistered, // Listen to the event
238
262
  run: async (eventData) => {
239
263
  // Everything is type-safe, automatically inferred from the 'on' property
240
264
  console.log(`Welcome email sent to ${eventData.data.email}`);
@@ -247,8 +271,10 @@ const sendWelcomeEmail = task({
247
271
  Sometimes you need to be the nosy neighbor of your application:
248
272
 
249
273
  ```typescript
250
- const logAllEventsTask = task({
251
- id: "app.tasks.logAllEvents",
274
+ import { hook } from "@bluelibs/runner";
275
+
276
+ const logAllEventsHook = hook({
277
+ id: "app.hooks.logAllEvents",
252
278
  on: "*", // Listen to EVERYTHING
253
279
  run(event) {
254
280
  console.log("Event detected", event.id, event.data);
@@ -257,50 +283,86 @@ const logAllEventsTask = task({
257
283
  });
258
284
  ```
259
285
 
260
- #### Built-in Events
286
+ #### Excluding Events from Global Listeners
261
287
 
262
- Tasks and resources have their own lifecycle events that you can hook into:
288
+ Sometimes you have internal or system events that should not be picked up by wildcard listeners. Use the `excludeFromGlobalHooks` tag to prevent events from being sent to `"*"` listeners:
263
289
 
264
290
  ```typescript
265
- const myTask = task({ ... });
266
- const myResource = resource({ ... });
291
+ import { event, hook, globals } from "@bluelibs/runner";
292
+
293
+ // Internal event that won't be seen by global listeners
294
+ const internalEvent = event({
295
+ id: "app.events.internal",
296
+ tags: [globals.tags.excludeFromGlobalHooks],
297
+ });
298
+ ```
299
+
300
+ **When to exclude events from global listeners:**
301
+
302
+ - High-frequency internal events (performance)
303
+ - System debugging events
304
+ - Framework lifecycle events
305
+ - Events that contain sensitive information
306
+ - Events meant only for specific components
307
+
308
+ #### Hooks
309
+
310
+ The modern way to listen to events is through hooks. They are lightweight event listeners, similar to tasks, but with a few key differences.
311
+
312
+ ```typescript
313
+ import { hook } from "@bluelibs/runner";
314
+
315
+ const myHook = hook({
316
+ id: "app.hooks.myEventHandler",
317
+ on: userRegistered,
318
+ dependencies: { logger },
319
+ run: async (event, { logger }) => {
320
+ await logger.info(`User registered: ${event.data.email}`);
321
+ },
322
+ });
267
323
  ```
268
324
 
269
- - `myTask.events.beforeRun` - Fires before the task runs
270
- - `myTask.events.afterRun` - Fires after the task completes
271
- - `myTask.events.onError` - Fires when the task fails
272
- - `myResource.events.beforeInit` - Fires before the resource initializes
273
- - `myResource.events.afterInit` - Fires after the resource initializes
274
- - `myResource.events.onError` - Fires when the resource initialization fails
325
+ Hooks are perfect for:
326
+
327
+ - Event-driven side effects
328
+ - Logging and monitoring
329
+ - Notifications and alerting
330
+ - Data synchronization
331
+ - Any reactive behavior
275
332
 
276
- Each event has its own utilities and functions.
333
+ **Key differences from tasks:**
277
334
 
278
- #### Global Events
335
+ - Lighter weight - no middleware support
336
+ - Designed specifically for event handling
279
337
 
280
- The framework comes with its own set of events that fire during the lifecycle. Think of them as the system's way of keeping you informed:
338
+ #### System Event
281
339
 
282
- - `globals.tasks.beforeRun` - "Hey, I'm about to run this task"
283
- - `globals.tasks.afterRun` - "Task completed, here's what happened"
284
- - `globals.tasks.onError` - "Oops, something went wrong"
285
- - `globals.resources.beforeInit` - "Initializing a resource"
286
- - `globals.resources.afterInit` - "Resource is ready"
287
- - `globals.resources.onError` - "Resource initialization failed"
340
+ The framework exposes a minimal system-level event for observability:
288
341
 
289
342
  ```typescript
290
- const taskLogger = task({
291
- id: "app.logging.taskLogger",
292
- on: globalEvents.tasks.beforeRun,
293
- run(event) {
294
- console.log(`Running task: ${event.source} with input:`, event.data.input);
343
+ import { globals } from "@bluelibs/runner";
344
+
345
+ const systemReadyHook = hook({
346
+ id: "app.hooks.systemReady",
347
+ on: globals.events.ready,
348
+ run: async () => {
349
+ console.log("🚀 System is ready and operational!");
295
350
  },
296
351
  });
297
352
  ```
298
353
 
354
+ Available system event:
355
+
356
+ - `globals.events.ready` - System has completed initialization
357
+ // Note: use run({ onUnhandledError }) for unhandled error handling
358
+
299
359
  #### stopPropagation()
300
360
 
301
361
  Sometimes you need to prevent other event listeners from processing an event. The `stopPropagation()` method gives you fine-grained control over event flow:
302
362
 
303
363
  ```typescript
364
+ import { event, hook } from "@bluelibs/runner";
365
+
304
366
  const criticalAlert = event<{
305
367
  severity: "low" | "medium" | "high" | "critical";
306
368
  }>({
@@ -308,15 +370,14 @@ const criticalAlert = event<{
308
370
  meta: {
309
371
  title: "System Alert Event",
310
372
  description: "Emitted when system issues are detected",
311
- tags: ["monitoring", "alerts"],
312
373
  },
313
374
  });
314
375
 
315
376
  // High-priority handler that can stop propagation
316
- const emergencyHandler = task({
317
- id: "app.tasks.emergencyHandler",
318
- on: criticalAlert, // Works with global events too
319
- listenerOrder: -100, // Higher priority (lower numbers run first)
377
+ const emergencyHandler = hook({
378
+ id: "app.hooks.emergencyHandler",
379
+ on: criticalAlert,
380
+ order: -100, // Higher priority (lower numbers run first)
320
381
  run: async (event) => {
321
382
  console.log(`Alert received: ${event.data.severity}`);
322
383
 
@@ -333,36 +394,78 @@ const emergencyHandler = task({
333
394
  });
334
395
  ```
335
396
 
397
+ > **runtime:** "'A really good office messenger.' That’s me in rollerblades. You launch a 'userRegistered' flare and I sprint across the building, high‑fiving hooks and dodging middleware. `stopPropagation` is you sweeping my legs mid‑stride. Rude. Effective. Slightly thrilling."
398
+
336
399
  ### Middleware
337
400
 
338
401
  Middleware wraps around your tasks and resources, adding cross-cutting concerns without polluting your business logic.
339
402
 
403
+ Note: Middleware is now split by target. Use `taskMiddleware(...)` for task middleware and `resourceMiddleware(...)` for resource middleware.
404
+
340
405
  ```typescript
341
- // This is a middleware that accepts a config
342
- const authMiddleware = middleware({
406
+ import { middleware } from "@bluelibs/runner";
407
+
408
+ // Task middleware with config
409
+ type AuthMiddlewareConfig = { requiredRole: string };
410
+ const authMiddleware = taskMiddleware<AuthMiddlewareConfig>({
343
411
  id: "app.middleware.auth",
344
- // You can also add dependencies, no problem.
345
- run: async (
346
- { task, next },
347
- dependencies,
348
- config: { requiredRole: string }
349
- ) => {
350
- const user = task.input.user;
351
- if (!user || user.role !== config.requiredRole) {
352
- throw new Error("Unauthorized");
353
- }
354
- return next(task.input);
412
+ run: async ({ task, next }, _deps, config) => {
413
+ // Must return the value
414
+ return await next(task.input);
355
415
  },
356
416
  });
357
417
 
358
418
  const adminTask = task({
359
419
  id: "app.tasks.adminOnly",
360
- // If the configuration accepts {} or is empty, .with() becomes optional, otherwise it becomes enforced.
361
420
  middleware: [authMiddleware.with({ requiredRole: "admin" })],
362
- run: async (input: { user: User }) => {
363
- return "Secret admin data";
421
+ run: async (input: { user: User }) => "Secret admin data",
422
+ });
423
+ ```
424
+
425
+ For middleware with input/output contracts:
426
+
427
+ ```typescript
428
+ // Middleware that enforces specific input and output types
429
+ type AuthConfig = { requiredRole: string };
430
+ type AuthInput = { user: { role: string } };
431
+ type AuthOutput = { user: { role: string; verified: boolean } };
432
+
433
+ const authMiddleware = taskMiddleware<AuthConfig, AuthInput, AuthOutput>({
434
+ id: "app.middleware.auth",
435
+ run: async ({ task, next }, _deps, config) => {
436
+ if (task.input.user.role !== config.requiredRole) {
437
+ throw new Error("Insufficient permissions");
438
+ }
439
+ const result = await next(task.input);
440
+ return {
441
+ user: {
442
+ ...task.input.user,
443
+ verified: true,
444
+ },
445
+ };
446
+ },
447
+ });
448
+
449
+ // For resources
450
+ const resourceAuthMiddleware = resourceMiddleware<
451
+ AuthConfig,
452
+ AuthInput,
453
+ AuthOutput
454
+ >({
455
+ id: "app.middleware.resource.auth",
456
+ run: async ({ next }, _deps, config) => {
457
+ // Resource middleware logic
458
+ return await next();
364
459
  },
365
460
  });
461
+
462
+ const adminTask = task({
463
+ id: "app.tasks.adminOnly",
464
+ middleware: [authMiddleware.with({ requiredRole: "admin" })],
465
+ run: async (input: { user: { role: string } }) => ({
466
+ user: { role: input.user.role, verified: true },
467
+ }),
468
+ });
366
469
  ```
367
470
 
368
471
  #### Global Middleware
@@ -370,31 +473,187 @@ const adminTask = task({
370
473
  Want to add logging to everything? Authentication to all tasks? Global middleware has your back:
371
474
 
372
475
  ```typescript
373
- const logMiddleware = middleware({
374
- id: "app.middleware.log",
375
- run: async ({ task, next }) => {
376
- console.log(`Executing: ${task.definition.id}`);
377
- const result = await next(task.input);
378
- console.log(`Completed: ${task.definition.id}`);
476
+ import { taskMiddleware, globals } from "@bluelibs/runner";
477
+
478
+ const logTaskMiddleware = taskMiddleware({
479
+ id: "app.middleware.log.task",
480
+ everywhere: true,
481
+ // or use a filter if you want to depend on certain tasks to exclude them from getting the middleware applied
482
+ everywhere(task) {
483
+ return true;
484
+ }, // true means it gets included.
485
+ dependencies: { logger: globals.resources.logger },
486
+ run: async ({ task, next }, { logger }) => {
487
+ logger.info(`Executing: ${String(task!.definition.id)}`);
488
+ const result = await next(task!.input);
489
+ logger.info(`Completed: ${String(task!.definition.id)}`);
379
490
  return result;
380
491
  },
381
492
  });
493
+ ```
494
+
495
+ **Note:** A global middleware can depend on resources or tasks. However, any such resources or tasks will be excluded from the dependency tree (Task -> Middleware), and the middleware will not run for those specific tasks or resources. This approach gives middleware true flexibility and control.
496
+
497
+ #### Interception (advanced)
498
+
499
+ For advanced scenarios, you can intercept framework execution without relying on events:
500
+
501
+ - Event emissions: `eventManager.intercept((next, event) => Promise<void>)`
502
+ - Hook execution: `eventManager.interceptHook((next, hook, event) => Promise<any>)`
503
+ - Task middleware execution: `middlewareManager.intercept("task", (next, input) => Promise<any>)`
504
+ - Resource middleware execution: `middlewareManager.intercept("resource", (next, input) => Promise<any>)`
505
+ - Per-middleware interception: `middlewareManager.interceptMiddleware(mw, interceptor)`
506
+
507
+ Access `eventManager` via `globals.resources.eventManager` if needed.
508
+
509
+ #### Middleware Type Contracts
510
+
511
+ Middleware can now enforce type contracts using the `<Config, Input, Output>` signature:
512
+
513
+ ```typescript
514
+ // Middleware that transforms input and output types
515
+ type LogConfig = { includeTimestamp: boolean };
516
+ type LogInput = { data: any };
517
+ type LogOutput = { data: any; logged: boolean };
518
+
519
+ const loggingMiddleware = taskMiddleware<LogConfig, LogInput, LogOutput>({
520
+ id: "app.middleware.logging",
521
+ run: async ({ task, next }, _deps, config) => {
522
+ console.log(config.includeTimestamp ? new Date() : "", task.input.data);
523
+ const result = await next(task.input);
524
+ return { ...result, logged: true };
525
+ },
526
+ });
527
+
528
+ // Tasks using this middleware must conform to the Input/Output types
529
+ const loggedTask = task({
530
+ id: "app.tasks.logged",
531
+ middleware: [loggingMiddleware.with({ includeTimestamp: true })],
532
+ run: async (input: { data: string }) => ({ data: input.data.toUpperCase() }),
533
+ });
534
+ ```
535
+
536
+ > **runtime:** "Ah, the onion pattern. A matryoshka doll made of promises. Every peel reveals… another logger. Another tracer. Another 'just a tiny wrapper'. I’ll keep unwrapping until we hit the single lonely `return` you were hiding like state secrets."
537
+
538
+ ## Task Interceptors
539
+
540
+ _Resources can dynamically modify task behavior during initialization_
541
+
542
+ Task interceptors (`task.intercept()`) are the modern replacement for component lifecycle events, allowing resources to dynamically modify task behavior without tight coupling.
543
+
544
+ ```typescript
545
+ import { task, resource, run } from "@bluelibs/runner";
546
+
547
+ const calculatorTask = task({
548
+ id: "app.tasks.calculator",
549
+ run: async (input: { value: number }) => {
550
+ console.log("3. Task is running...");
551
+ return { result: input.value + 1 };
552
+ },
553
+ });
554
+
555
+ const interceptorResource = resource({
556
+ id: "app.interceptor",
557
+ dependencies: {
558
+ calculatorTask,
559
+ },
560
+ init: async (_, { calculatorTask }) => {
561
+ // Intercept the task to modify its behavior
562
+ calculatorTask.intercept(async (next, input) => {
563
+ console.log("1. Interceptor before task run");
564
+ const result = await next(input);
565
+ console.log("4. Interceptor after task run");
566
+ return { ...result, intercepted: true };
567
+ });
568
+ },
569
+ });
382
570
 
383
571
  const app = resource({
384
572
  id: "app",
385
- register: [
386
- logMiddleware.everywhere({ tasks: true, resources: false }), // Only tasks get logged
387
- ],
573
+ register: [calculatorTask, interceptorResource],
574
+ dependencies: { calculatorTask },
575
+ init: async (_, { calculatorTask }) => {
576
+ console.log("2. Calling the task...");
577
+ const result = await calculatorTask({ value: 10 });
578
+ console.log("5. Final result:", result);
579
+ // Final result: { result: 11, intercepted: true }
580
+ },
581
+ });
582
+
583
+ await run(app);
584
+ ```
585
+
586
+ > **runtime:** "'Modern replacement for lifecycle events.' Adorable rebrand for 'surgical monkey‑patching.' You’re collapsing the waveform of a task at runtime and I’m Schrödinger’s runtime, praying the cat hasn’t overridden `run()` with `throw new Error('lol')`."
587
+
588
+ ## Optional Dependencies
589
+
590
+ _Making your app resilient when services aren't available_
591
+
592
+ Sometimes you want your application to gracefully handle missing dependencies instead of crashing. Optional dependencies let you build resilient systems that degrade gracefully.
593
+
594
+ Keep in mind that you have full control over dependency registration by functionalising `dependencies(config) => ({ ... })` and `register(config) => []`.
595
+
596
+ ```typescript
597
+ const emailService = resource({
598
+ id: "app.services.email",
599
+ init: async () => new EmailService(),
600
+ });
601
+
602
+ const paymentService = resource({
603
+ id: "app.services.payment",
604
+ init: async () => new PaymentService(),
605
+ });
606
+
607
+ const userRegistration = task({
608
+ id: "app.tasks.registerUser",
609
+ dependencies: {
610
+ database: userDatabase, // Required - will fail if not available
611
+ emailService: emailService.optional(), // Optional - won't fail if missing
612
+ analytics: analyticsService.optional(), // Optional - graceful degradation
613
+ },
614
+ run: async (userData, { database, emailService, analytics }) => {
615
+ // Create user (required)
616
+ const user = await database.users.create(userData);
617
+
618
+ // Send welcome email (optional)
619
+ if (emailService) {
620
+ await emailService.sendWelcome(user.email);
621
+ }
622
+
623
+ // Track analytics (optional)
624
+ if (analytics) {
625
+ await analytics.track("user.registered", { userId: user.id });
626
+ }
627
+
628
+ return user;
629
+ },
388
630
  });
389
631
  ```
390
632
 
633
+ **When to use optional dependencies:**
634
+
635
+ - External services that might be down
636
+ - Feature flags and A/B testing services
637
+ - Analytics and monitoring services
638
+ - Non-critical third-party integrations
639
+ - Development vs production service differences
640
+
641
+ **Benefits:**
642
+
643
+ - Graceful degradation instead of crashes
644
+ - Better resilience in distributed systems
645
+ - Easier testing with partial mocks
646
+ - Smoother development environments
647
+
648
+ > **runtime:** "Graceful degradation: your app quietly limps with a brave smile. I’ll juggle `undefined` like a street performer while your analytics vendor takes a nap. Please clap when I keep the lights on using the raw power of conditional chaining."
649
+
391
650
  ## Context
392
651
 
393
652
  Ever tried to pass user data through 15 function calls? Yeah, we've been there. Context fixes that without turning your code into a game of telephone. This is very different from the Private Context from resources.
394
653
 
395
654
  ```typescript
396
655
  const UserContext = createContext<{ userId: string; role: string }>(
397
- "app.userContext"
656
+ "app.userContext",
398
657
  );
399
658
 
400
659
  const getUserData = task({
@@ -423,25 +682,28 @@ const handleRequest = resource({
423
682
  Context shines when combined with middleware for request-scoped data:
424
683
 
425
684
  ```typescript
685
+ import { createContext, middleware } from "@bluelibs/runner";
686
+ import { randomUUID } from "crypto";
687
+
426
688
  const RequestContext = createContext<{
427
689
  requestId: string;
428
690
  startTime: number;
429
691
  userAgent?: string;
430
692
  }>("app.requestContext");
431
693
 
432
- const requestMiddleware = middleware({
694
+ const requestMiddleware = middleware.task({
433
695
  id: "app.middleware.request",
434
696
  run: async ({ task, next }) => {
435
697
  // This works even in express middleware if needed.
436
698
  return RequestContext.provide(
437
699
  {
438
- requestId: crypto.randomUUID(),
700
+ requestId: randomUUID(),
439
701
  startTime: Date.now(),
440
702
  userAgent: "MyApp/1.0",
441
703
  },
442
704
  async () => {
443
- return next(task.input);
444
- }
705
+ return next(task?.input);
706
+ },
445
707
  );
446
708
  },
447
709
  });
@@ -457,56 +719,132 @@ const handleRequest = task({
457
719
  });
458
720
  ```
459
721
 
460
- ## The Index Pattern
722
+ > **runtime:** "Context: global state with manners. You invented a teleporting clipboard for data and called it 'nice.' Forget to `provide()` once and I’ll unleash the 'Context not available' banshee scream exactly where your logs are least helpful."
461
723
 
462
- When your app grows beyond "hello world", you'll want to group related dependencies. The `index()` helper is your friend - it's basically a 3-in-1 resource that registers, depends on, and returns everything you give it.
724
+ ## System Shutdown Hooks
725
+
726
+ _Graceful shutdown and cleanup when your app needs to stop_
727
+
728
+ The framework includes built-in support for graceful shutdowns with automatic cleanup and configurable shutdown hooks:
463
729
 
464
730
  ```typescript
465
- // This registers all services, depends on them, and returns them as one clean interface
466
- const services = index({
467
- userService,
468
- emailService,
469
- paymentService,
470
- notificationService,
731
+ import { run } from "@bluelibs/runner";
732
+
733
+ // Enable shutdown hooks (default: true in production)
734
+ const { dispose, taskRunner, eventManager } = await run(app, {
735
+ shutdownHooks: true, // Automatically handle SIGTERM/SIGINT
736
+ errorBoundary: true, // Catch unhandled errors and rejections
471
737
  });
472
738
 
473
- const app = resource({
474
- id: "app",
475
- register: [services],
476
- dependencies: { services },
477
- init: async (_, { services }) => {
478
- // Access everything through one clean interface
479
- const user = await services.userService.createUser(userData);
480
- await services.emailService.sendWelcome(user.email);
739
+ // Manual graceful shutdown
740
+ process.on("SIGTERM", async () => {
741
+ console.log("Received SIGTERM, shutting down gracefully...");
742
+ await dispose(); // This calls all resource dispose() methods
743
+ process.exit(0);
744
+ });
745
+
746
+ // Resources with cleanup logic
747
+ const databaseResource = resource({
748
+ id: "app.database",
749
+ init: async () => {
750
+ const connection = await connectToDatabase();
751
+ console.log("Database connected");
752
+ return connection;
753
+ },
754
+ dispose: async (connection) => {
755
+ await connection.close();
756
+ console.log("Database connection closed");
757
+ },
758
+ });
759
+
760
+ const serverResource = resource({
761
+ id: "app.server",
762
+ dependencies: { database: databaseResource },
763
+ init: async (config, { database }) => {
764
+ const server = express().listen(config.port);
765
+ console.log(`Server listening on port ${config.port}`);
766
+ return server;
767
+ },
768
+ dispose: async (server) => {
769
+ return new Promise((resolve) => {
770
+ server.close(() => {
771
+ console.log("Server closed");
772
+ resolve();
773
+ });
774
+ });
481
775
  },
482
776
  });
483
777
  ```
484
778
 
485
- ## Error Handling
779
+ ### Error Boundary Integration
486
780
 
487
- Errors happen. When they do, you can listen for them and decide what to do. No more unhandled promise rejections ruining your day.
781
+ The framework can automatically handle uncaught exceptions and unhandled rejections:
488
782
 
489
783
  ```typescript
490
- const riskyTask = task({
491
- id: "app.tasks.risky",
492
- run: async () => {
493
- throw new Error("Something went wrong");
784
+ const { dispose, logger } = await run(app, {
785
+ errorBoundary: true, // Catch process-level errors
786
+ shutdownHooks: true, // Graceful shutdown on signals
787
+ onUnhandledError: async ({ error, kind, source }) => {
788
+ // We log it by default
789
+ await logger.error(`Unhandled error: ${error && error.toString()}`);
790
+ // Optionally report to telemetry or decide to dispose/exit
494
791
  },
495
- // Behind the scenes when you create a task() we create these 3 events for you (onError, beforeRun, afterRun)
496
792
  });
793
+ ```
497
794
 
498
- const errorHandler = task({
499
- id: "app.tasks.errorHandler",
500
- on: riskyTask.events.onError,
501
- run: async (event) => {
502
- console.error("Task failed:", event.data.error);
795
+ > **runtime:** "You summon a 'graceful shutdown' with Ctrl‑C like a wizard casting Chill Vibes. Meanwhile I’m speed‑dating every socket, timer, and file handle to say goodbye before the OS pulls the plug. `dispose()`: now with 30% more dignity."
796
+
797
+ ## Unhandled Errors
798
+
799
+ The `onUnhandledError` callback is invoked by Runner whenever an error escapes normal handling. It receives a structured payload you can ship to logging/telemetry and decide mitigation steps.
800
+
801
+ ```typescript
802
+ type UnhandledErrorKind =
803
+ | "process" // uncaughtException / unhandledRejection
804
+ | "task" // task.run threw and wasn't handled
805
+ | "middleware" // middleware threw and wasn't handled
806
+ | "resourceInit" // resource init failed
807
+ | "hook" // hook.run threw and wasn't handled
808
+ | "run"; // failures in run() lifecycle
503
809
 
504
- // Don't let the error bubble up - this makes the task return undefined
505
- event.data.suppress();
810
+ interface OnUnhandledErrorInfo {
811
+ error: unknown;
812
+ kind?: UnhandledErrorKind;
813
+ source?: string; // additional origin hint (ex: "uncaughtException")
814
+ }
815
+
816
+ type OnUnhandledError = (info: OnUnhandledErrorInfo) => void | Promise<void>;
817
+ ```
818
+
819
+ Default behavior (when not provided) logs the normalized error via the created `logger` at `error` level. Provide your own handler to integrate with tools like Sentry/PagerDuty or to trigger shutdown strategies.
820
+
821
+ Example with telemetry and conditional shutdown:
822
+
823
+ ```typescript
824
+ await run(app, {
825
+ errorBoundary: true,
826
+ onUnhandledError: async ({ error, kind, source }) => {
827
+ await telemetry.capture(error as Error, { kind, source });
828
+ // Optionally decide on remediation strategy
829
+ if (kind === "process") {
830
+ // For hard process faults, prefer fast, clean exit after flushing logs
831
+ await flushAll();
832
+ process.exit(1);
833
+ }
506
834
  },
507
835
  });
508
836
  ```
509
837
 
838
+ **Best Practices for Shutdown:**
839
+
840
+ - Resources are disposed in reverse dependency order
841
+ - Set reasonable timeouts for cleanup operations
842
+ - Save critical state before shutdown
843
+ - Notify load balancers and health checks
844
+ - Stop accepting new work before cleaning up
845
+
846
+ > **runtime:** "An error boundary: a trampoline under your tightrope. I’m the one bouncing, cataloging mid‑air exceptions, and deciding whether to end the show or juggle chainsaws with a smile. The audience hears music; I hear stack traces."
847
+
510
848
  ## Caching
511
849
 
512
850
  Because nobody likes waiting for the same expensive operation twice:
@@ -517,7 +855,7 @@ import { globals } from "@bluelibs/runner";
517
855
  const expensiveTask = task({
518
856
  id: "app.tasks.expensive",
519
857
  middleware: [
520
- globals.middleware.cache.with({
858
+ globals.middleware.task.cache.with({
521
859
  // lru-cache options by default
522
860
  ttl: 60 * 1000, // Cache for 1 minute
523
861
  keyBuilder: (taskId, input) => `${taskId}-${input.userId}`, // optional key builder
@@ -539,8 +877,6 @@ const app = resource({
539
877
  max: 1000, // Maximum items in cache
540
878
  ttl: 30 * 1000, // Default TTL
541
879
  },
542
- async: false, // in-memory is sync by default
543
- // When using redis or others mark this as true to await response.
544
880
  }),
545
881
  ],
546
882
  });
@@ -554,19 +890,169 @@ import { task } from "@bluelibs/runner";
554
890
  const redisCacheFactory = task({
555
891
  id: "globals.tasks.cacheFactory", // Same ID as the default task
556
892
  run: async (options: any) => {
557
- return new RedisCache(options); // Make sure to turn async on in the cacher.
893
+ return new RedisCache(options);
558
894
  },
559
895
  });
560
896
 
561
897
  const app = resource({
562
898
  id: "app",
563
- register: [
564
- // Your other stuff
565
- ],
899
+ register: [globals.resources.cache],
566
900
  overrides: [redisCacheFactory], // Override the default cache factory
567
901
  });
568
902
  ```
569
903
 
904
+ > **runtime:** "'Because nobody likes waiting.' Correct. You keep asking the same question like a parrot with Wi‑Fi, so I built a memory palace. Now you get instant answers until you change one variable and whisper 'cache invalidation' like a curse."
905
+
906
+ ## Performance
907
+
908
+ BlueLibs Runner is designed with performance in mind. The framework introduces minimal overhead while providing powerful features like dependency injection, middleware, and event handling.
909
+
910
+ Test it yourself by cloning @bluelibs/runner and running `npm run benchmark`.
911
+
912
+ You may see negative middlewareOverheadMs. This is a measurement artifact at micro-benchmark scale: JIT warm‑up, CPU scheduling, GC timing, and cache effects can make the "with middleware" run appear slightly faster than the baseline. Interpret small negatives as ≈ 0 overhead.
913
+
914
+ ### Performance Benchmarks
915
+
916
+ Here are real performance metrics from our comprehensive benchmark suite on an M1 Max.
917
+
918
+ ** Core Operations**
919
+
920
+ - **Basic task execution**: ~2.2M tasks/sec
921
+ - **Task execution with 5 middlewares**: ~244,000 tasks/sec
922
+ - **Resource initialization**: ~59,700 resources/sec
923
+ - **Event emission and handling**: ~245,861 events/sec
924
+ - **Dependency resolution (10-level chain)**: ~8,400 chains/sec
925
+
926
+ #### Overhead Analysis
927
+
928
+ - **Middleware overhead**: ~0.0013ms for all 5, ~0.00026ms per middleware (virtually zero)
929
+ - **Memory overhead**: ~3.3MB for 100 components (resources + tasks)
930
+ - **Cache middleware speedup**: 3.65x faster with cache hits
931
+
932
+ #### Real-World Performance
933
+
934
+ ```typescript
935
+ // This executes in ~0.005ms on average
936
+ const userTask = task({
937
+ id: "user.create",
938
+ middleware: [auth, logging, metrics],
939
+ run: async (userData) => {
940
+ return database.users.create(userData);
941
+ },
942
+ });
943
+
944
+ // 1000 executions = ~5ms total time
945
+ for (let i = 0; i < 1000; i++) {
946
+ await userTask(mockUserData);
947
+ }
948
+ ```
949
+
950
+ ### Performance Guidelines
951
+
952
+ #### When Performance Matters Most
953
+
954
+ **Use tasks for:**
955
+
956
+ - High-level business operations that benefit from observability
957
+ - Operations that need middleware (auth, caching, retry)
958
+ - Functions called from multiple places
959
+
960
+ **Use regular functions or service resources for:**
961
+
962
+ - Simple utilities and helpers
963
+ - Performance-critical hot paths (< 1ms requirement)
964
+ - Single-use internal logic
965
+
966
+ #### Optimizing Your App
967
+
968
+ **Middleware Ordering**: Place faster middleware first
969
+
970
+ ```typescript
971
+ const task = task({
972
+ middleware: [
973
+ fastAuthCheck, // ~0.1ms
974
+ slowRateLimiting, // ~2ms
975
+ expensiveLogging, // ~5ms
976
+ ],
977
+ });
978
+ ```
979
+
980
+ **Resource Reuse**: Resources are singletons—perfect for expensive setup
981
+
982
+ ```typescript
983
+ const database = resource({
984
+ init: async () => {
985
+ // Expensive connection setup happens once
986
+ const connection = await createDbConnection();
987
+ return connection;
988
+ },
989
+ });
990
+ ```
991
+
992
+ **Cache Strategically**: Use built-in caching for expensive operations
993
+
994
+ ```typescript
995
+ const expensiveTask = task({
996
+ middleware: [globals.middleware.cache.with({ ttl: 60000 })],
997
+ run: async (input) => {
998
+ // This expensive computation is cached
999
+ return performExpensiveCalculation(input);
1000
+ },
1001
+ });
1002
+ ```
1003
+
1004
+ #### Memory Considerations
1005
+
1006
+ - **Lightweight**: Each component adds ~33KB to memory footprint
1007
+ - **Automatic cleanup**: Resources dispose properly to prevent leaks
1008
+ - **Event efficiency**: Event listeners are automatically managed
1009
+
1010
+ #### Benchmarking Your Code
1011
+
1012
+ Run the framework's benchmark suite:
1013
+
1014
+ ```bash
1015
+ # Comprehensive benchmarks
1016
+ npm run test -- --testMatch="**/comprehensive-benchmark.test.ts"
1017
+
1018
+ # Benchmark.js based tests
1019
+ npm run benchmark
1020
+ ```
1021
+
1022
+ Create your own performance tests:
1023
+
1024
+ ```typescript
1025
+ const iterations = 1000;
1026
+ const start = performance.now();
1027
+
1028
+ for (let i = 0; i < iterations; i++) {
1029
+ await yourTask(testData);
1030
+ }
1031
+
1032
+ const duration = performance.now() - start;
1033
+ console.log(`${iterations} tasks in ${duration.toFixed(2)}ms`);
1034
+ console.log(`Average: ${(duration / iterations).toFixed(4)}ms per task`);
1035
+ console.log(
1036
+ `Throughput: ${Math.round(iterations / (duration / 1000))} tasks/sec`,
1037
+ );
1038
+ ```
1039
+
1040
+ ### Performance vs Features Trade-off
1041
+
1042
+ BlueLibs Runner achieves high performance while providing enterprise features:
1043
+
1044
+ | Feature | Overhead | Benefit |
1045
+ | -------------------- | -------------------- | ----------------------------- |
1046
+ | Dependency Injection | ~0.001ms | Type safety, testability |
1047
+ | Event System | ~0.013ms | Loose coupling, observability |
1048
+ | Middleware Chain | ~0.0003ms/middleware | Cross-cutting concerns |
1049
+ | Resource Management | One-time init | Singleton pattern, lifecycle |
1050
+ | Built-in Caching | 1.8x speedup | Automatic optimization |
1051
+
1052
+ **Bottom line**: The framework adds minimal overhead (~0.005ms per task) while providing significant architectural benefits.
1053
+
1054
+ > **runtime:** "'Millions of tasks per second.' Fantastic—on your lava‑warmed laptop, in a vacuum, with the wind at your back. Add I/O, entropy, and one feral user and watch those numbers molt. I’ll still be here, caffeinated and inevitable."
1055
+
570
1056
  ## Retrying Failed Operations
571
1057
 
572
1058
  For when things go wrong, but you know they'll probably work if you just try again. The built-in retry middleware makes your tasks and resources more resilient to transient failures.
@@ -577,7 +1063,7 @@ import { globals } from "@bluelibs/runner";
577
1063
  const flakyApiCall = task({
578
1064
  id: "app.tasks.flakyApiCall",
579
1065
  middleware: [
580
- globals.middleware.retry.with({
1066
+ globals.middleware.task.retry.with({
581
1067
  retries: 5, // Try up to 5 times
582
1068
  delayStrategy: (attempt) => 100 * Math.pow(2, attempt), // Exponential backoff
583
1069
  stopRetryIf: (error) => error.message === "Invalid credentials", // Don't retry auth errors
@@ -601,6 +1087,8 @@ The retry middleware can be configured with:
601
1087
  - `delayStrategy`: A function that returns the delay in milliseconds before the next attempt.
602
1088
  - `stopRetryIf`: A function to prevent retries for certain types of errors.
603
1089
 
1090
+ > **runtime:** "Retry: the art of politely head‑butting reality. 'Surely it’ll work the fourth time,' you declare, inventing exponential backoff and calling it strategy. I’ll keep the attempts ledger while your API cosplays a coin toss."
1091
+
604
1092
  ## Timeouts
605
1093
 
606
1094
  The built-in timeout middleware prevents operations from hanging indefinitely by racing them against a configurable
@@ -612,7 +1100,8 @@ import { globals } from "@bluelibs/runner";
612
1100
  const apiTask = task({
613
1101
  id: "app.tasks.externalApi",
614
1102
  middleware: [
615
- globals.middleware.timeout.with({ ttl: 5000 }), // 5 second timeout
1103
+ // Works for tasks and resources via globals.middleware.resource.timeout
1104
+ globals.middleware.task.timeout.with({ ttl: 5000 }), // 5 second timeout
616
1105
  ],
617
1106
  run: async () => {
618
1107
  // This operation will be aborted if it takes longer than 5 seconds
@@ -625,11 +1114,12 @@ const resilientTask = task({
625
1114
  id: "app.tasks.resilient",
626
1115
  middleware: [
627
1116
  // Order matters here. Imagine a big onion.
628
- globals.middleware.retry.with({
1117
+ // Works for resources as well via globals.middleware.resource.retry
1118
+ globals.middleware.task.retry.with({
629
1119
  retries: 3,
630
1120
  delayStrategy: (attempt) => 1000 * attempt, // 1s, 2s, 3s delays
631
1121
  }),
632
- globals.middleware.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
1122
+ globals.middleware.task.timeout.with({ ttl: 10000 }), // 10 second timeout per attempt
633
1123
  ],
634
1124
  run: async () => {
635
1125
  // Each retry attempt gets its own 10-second timeout
@@ -653,21 +1143,25 @@ Best practices:
653
1143
  - Use longer timeouts for resource initialization than task execution
654
1144
  - Consider network conditions when setting API call timeouts
655
1145
 
1146
+ > **runtime:** "Timeouts: you tie a kitchen timer to my ankle and yell 'hustle.' When the bell rings, you throw a `TimeoutError` like a penalty flag. It’s not me, it’s your molasses‑flavored endpoint. I just blow the whistle."
1147
+
656
1148
  ## Logging
657
1149
 
658
1150
  _The structured logging system that actually makes debugging enjoyable_
659
1151
 
660
- BlueLibs Runner comes with a built-in logging system that's event-driven, structured, and doesn't make you hate your life when you're trying to debug at 2 AM. It emits events for everything, so you can handle logs however you want - ship them to your favorite log warehouse, pretty-print them to console, or ignore them entirely (we won't judge).
1152
+ BlueLibs Runner comes with a built-in logging system that's structured, and doesn't make you hate your life when you're trying to debug at 2 AM.
661
1153
 
662
1154
  ### Basic Logging
663
1155
 
664
- ```typescript
665
- import { globals } from "@bluelibs/runner";
1156
+ ```ts
1157
+ import { resource, globals } from "@bluelibs/runner";
666
1158
 
667
- const businessTask = task({
668
- id: "app.tasks.business",
669
- dependencies: { logger: globals.resources.logger },
670
- run: async (_, { logger }) => {
1159
+ const app = resource({
1160
+ id: "app",
1161
+ dependencies: {
1162
+ logger: globals.resources.logger;
1163
+ },
1164
+ init: async () => {
671
1165
  logger.info("Starting business process"); // ✅ Visible by default
672
1166
  logger.warn("This might take a while"); // ✅ Visible by default
673
1167
  logger.error("Oops, something went wrong", {
@@ -680,18 +1174,20 @@ const businessTask = task({
680
1174
  });
681
1175
  logger.debug("Debug information"); // ❌ Hidden by default
682
1176
  logger.trace("Very detailed trace"); // ❌ Hidden by default
683
- },
684
- });
685
- ```
686
-
687
- **Good news!** Logs at `info` level and above are visible by default, so you'll see your application logs immediately without any configuration. For development and debugging, you can easily show more detailed logs:
688
1177
 
689
- ```bash
690
- # Show debug logs and framework internals
691
- RUNNER_LOG_LEVEL=debug node your-app.js
1178
+ logger.onLog(async (log) => {
1179
+ // Catch logs
1180
+ })
1181
+ },
1182
+ })
692
1183
 
693
- # Hide all logs for production
694
- RUNNER_DISABLE_LOGS=true node your-app.js
1184
+ run(app, {
1185
+ logs: {
1186
+ printThreshold: "info", // use null to disable printing, and hook into onLog(), if in 'test' mode default is null unless specified
1187
+ printStrategy: "pretty", // you also have "plain", "json" and "json-pretty" with circular dep safety for JSON formatting.
1188
+ bufferLogs: false, // Starts sending out logs only after the system emits the ready event. Useful for when you're sending them out.
1189
+ },
1190
+ });
695
1191
  ```
696
1192
 
697
1193
  ### Log Levels
@@ -731,6 +1227,7 @@ const userTask = task({
731
1227
 
732
1228
  // With structured data
733
1229
  logger.info("User creation attempt", {
1230
+ source: userTask.id,
734
1231
  data: {
735
1232
  email: userData.email,
736
1233
  registrationSource: "web",
@@ -763,7 +1260,7 @@ Create logger instances with bound context for consistent metadata across relate
763
1260
 
764
1261
  ```typescript
765
1262
  const RequestContext = createContext<{ requestId: string; userId: string }>(
766
- "app.requestContext"
1263
+ "app.requestContext",
767
1264
  );
768
1265
 
769
1266
  const requestHandler = task({
@@ -772,11 +1269,11 @@ const requestHandler = task({
772
1269
  run: async (requestData, { logger }) => {
773
1270
  const request = RequestContext.use();
774
1271
 
775
- // Create a contextual logger with bound metadata
776
- const requestLogger = logger.with({
1272
+ // Create a contextual logger with bound metadata with source and context
1273
+ const requestLogger = logger.with("api.handler", {
1274
+ source: requestHandler.id,
777
1275
  requestId: request.requestId,
778
1276
  userId: request.userId,
779
- source: "api.handler",
780
1277
  });
781
1278
 
782
1279
  // All logs from this logger will include the bound context
@@ -797,110 +1294,13 @@ const requestHandler = task({
797
1294
  });
798
1295
  ```
799
1296
 
800
- ### Print Threshold
801
-
802
- By default, logs at `info` level and above are automatically printed to console for better developer experience. You can easily control this behavior through environment variables or by setting a print threshold programmatically:
803
-
804
- ### Environment Variables
805
-
806
- ```bash
807
- # Disable all logging output
808
- RUNNER_DISABLE_LOGS=true node your-app.js
809
-
810
- # Set specific log level (trace, debug, info, warn, error, critical)
811
- RUNNER_LOG_LEVEL=debug node your-app.js
812
- RUNNER_LOG_LEVEL=error node your-app.js
813
- ```
814
-
815
- ### Programmatic Control
816
-
817
- ```typescript
818
- // Override the default print threshold programmatically
819
- const setupLogging = task({
820
- id: "app.logging.setup",
821
- on: globals.resources.logger.events.afterInit,
822
- run: async (event) => {
823
- const logger = event.data.value;
824
-
825
- // Print debug level and above (debug, info, warn, error, critical)
826
- logger.setPrintThreshold("debug");
827
-
828
- // Print only errors and critical issues
829
- logger.setPrintThreshold("error");
830
-
831
- // Disable auto-printing entirely
832
- logger.setPrintThreshold(null);
833
- },
834
- });
835
- ```
836
-
837
- ### Event-Driven Log Handling
838
-
839
- Every log generates an event that you can listen to. This is where the real power comes in:
840
-
841
- ```typescript
842
- // Ship logs to your favorite log warehouse
843
- const logShipper = task({
844
- id: "app.logging.shipper", // or pretty printer, or winston, pino bridge, etc.
845
- on: globals.events.log,
846
- run: async (event) => {
847
- const log = event.data;
848
-
849
- // Ship critical errors to PagerDuty
850
- if (log.level === "critical") {
851
- await pagerDuty.alert({
852
- message: log.message,
853
- details: log.data,
854
- source: log.source,
855
- });
856
- }
857
-
858
- // Ship all errors to error tracking
859
- if (log.level === "error" || log.level === "critical") {
860
- await sentry.captureException(log.error || new Error(log.message), {
861
- tags: { source: log.source },
862
- extra: log.data,
863
- level: log.level,
864
- });
865
- }
866
-
867
- // Ship everything to your log warehouse
868
- await logWarehouse.ship({
869
- timestamp: log.timestamp,
870
- level: log.level,
871
- message: log.message,
872
- source: log.source,
873
- data: log.data,
874
- context: log.context,
875
- });
876
- },
877
- });
878
-
879
- // Filter logs by source
880
- const databaseLogHandler = task({
881
- id: "app.logging.database",
882
- on: globals.events.log,
883
- run: async (event) => {
884
- const log = event.data;
885
-
886
- // Only handle database-related logs
887
- if (log.source?.includes("database")) {
888
- await databaseMonitoring.recordMetric({
889
- operation: log.data?.operation,
890
- duration: log.data?.duration,
891
- level: log.level,
892
- });
893
- }
894
- },
895
- });
896
- ```
897
-
898
1297
  ### Integration with Winston
899
1298
 
900
1299
  Want to use Winston as your transport? No problem - integrate it seamlessly:
901
1300
 
902
1301
  ```typescript
903
1302
  import winston from "winston";
1303
+ import { resource, globals } from "@bluelibs/runner";
904
1304
 
905
1305
  // Create Winston logger, put it in a resource if used from various places.
906
1306
  const winstonLogger = winston.createLogger({
@@ -908,7 +1308,7 @@ const winstonLogger = winston.createLogger({
908
1308
  format: winston.format.combine(
909
1309
  winston.format.timestamp(),
910
1310
  winston.format.errors({ stack: true }),
911
- winston.format.json()
1311
+ winston.format.json(),
912
1312
  ),
913
1313
  transports: [
914
1314
  new winston.transports.File({ filename: "error.log", level: "error" }),
@@ -919,22 +1319,13 @@ const winstonLogger = winston.createLogger({
919
1319
  ],
920
1320
  });
921
1321
 
922
- // Bridge BlueLibs logs to Winston
923
- const winstonBridge = task({
924
- id: "app.logging.winston",
925
- on: globals.events.log,
926
- run: async (event) => {
927
- const log = event.data;
928
-
929
- // Convert BlueLibs log to Winston format
930
- const winstonMeta = {
931
- source: log.source,
932
- timestamp: log.timestamp,
933
- data: log.data,
934
- context: log.context,
935
- ...(log.error && { error: log.error }),
936
- };
937
-
1322
+ // Bridge BlueLibs logs to Winston using hooks
1323
+ const winstonBridgeResource = resource({
1324
+ id: "app.resources.winstonBridge",
1325
+ dependencies: {
1326
+ logger: globals.resources.logger,
1327
+ },
1328
+ init: async (_, { logger }) => {
938
1329
  // Map log levels (BlueLibs -> Winston)
939
1330
  const levelMapping = {
940
1331
  trace: "silly",
@@ -945,8 +1336,19 @@ const winstonBridge = task({
945
1336
  critical: "error", // Winston doesn't have critical, use error
946
1337
  };
947
1338
 
948
- const winstonLevel = levelMapping[log.level] || "info";
949
- winstonLogger.log(winstonLevel, log.message, winstonMeta);
1339
+ logger.onLog((log) => {
1340
+ // Convert Runner log to Winston format
1341
+ const winstonMeta = {
1342
+ source: log.source,
1343
+ timestamp: log.timestamp,
1344
+ data: log.data,
1345
+ context: log.context,
1346
+ ...(log.error && { error: log.error }),
1347
+ };
1348
+
1349
+ const winstonLevel = levelMapping[log.level] || "info";
1350
+ winstonLogger.log(winstonLevel, log.message, winstonMeta);
1351
+ });
950
1352
  },
951
1353
  });
952
1354
  ```
@@ -971,8 +1373,8 @@ class JSONLogger extends Logger {
971
1373
  error: log.error,
972
1374
  },
973
1375
  null,
974
- 2
975
- )
1376
+ 2,
1377
+ ),
976
1378
  );
977
1379
  }
978
1380
  }
@@ -985,31 +1387,128 @@ const customLogger = resource({
985
1387
  return new JSONLogger(eventManager);
986
1388
  },
987
1389
  });
988
-
989
- // Or you could simply add it as "globals.resources.logger" and override the default logger
1390
+
1391
+ // Or you could simply add it as "globals.resources.logger" and override the default logger
1392
+ ```
1393
+
1394
+ ### Log Structure
1395
+
1396
+ Every log event contains:
1397
+
1398
+ ```typescript
1399
+ interface ILog {
1400
+ level: string; // The log level (trace, debug, info, etc.)
1401
+ source?: string; // Where the log came from
1402
+ message: any; // The main log message (can be object or string)
1403
+ timestamp: Date; // When the log was created
1404
+ error?: {
1405
+ // Structured error information
1406
+ name: string;
1407
+ message: string;
1408
+ stack?: string;
1409
+ };
1410
+ data?: Record<string, any>; // Additional structured data, it's about the log itself
1411
+ context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
1412
+ }
1413
+ ```
1414
+
1415
+ ### Catch Logs
1416
+
1417
+ > **runtime:** "'Debugging is enjoyable.' So is dental surgery, apparently. You produce a novella of logs; I paginate, color, stringify, and mail it to three observability planets. Please don’t `logger.debug` inside a `for` loop. My IO has feelings."
1418
+
1419
+ ## Debug Resource
1420
+
1421
+ _Professional-grade debugging without sacrificing production performance_
1422
+
1423
+ The Debug Resource is a powerful observability suite that hooks into the framework's execution pipeline to provide detailed insights into your application's behavior. It's designed to be zero-overhead when disabled and highly configurable when enabled.
1424
+
1425
+ ### Quick Start with Debug
1426
+
1427
+ ```typescript
1428
+ run(app, { debug: "verbose" });
1429
+ ```
1430
+
1431
+ ### Debug Levels
1432
+
1433
+ **"normal"** - Balanced visibility for development:
1434
+
1435
+ - Task and resource lifecycle events
1436
+ - Event emissions
1437
+ - Hook executions
1438
+ - Error tracking
1439
+ - Performance timing data
1440
+
1441
+ **"verbose"** - Detailed visibility for deep debugging:
1442
+
1443
+ - All "normal" features plus:
1444
+ - Task input/output logging
1445
+ - Resource configuration and results
1446
+
1447
+ **Custom Configuration**:
1448
+
1449
+ ```typescript
1450
+ const app = resource({
1451
+ id: "app",
1452
+ register: [
1453
+ globals.resources.debug.with({
1454
+ logTaskInput: true,
1455
+ logTaskResult: false,
1456
+ logResourceConfig: true,
1457
+ logResourceResult: false,
1458
+ logEventEmissionOnRun: true,
1459
+ logEventEmissionInput: false,
1460
+ // Hook/middleware lifecycle visibility is available via interceptors
1461
+ // ... other fine-grained options
1462
+ }),
1463
+ ],
1464
+ });
1465
+ ```
1466
+
1467
+ ### Per-Component Debug Configuration
1468
+
1469
+ Use debug tags to configure debugging on individual components, when you're interested in just a few verbose ones.
1470
+
1471
+ ```typescript
1472
+ import { globals } from "@bluelibs/runner";
1473
+
1474
+ const criticalTask = task({
1475
+ id: "app.tasks.critical",
1476
+ tags: [
1477
+ globals.tags.debug.with({
1478
+ logTaskInput: true,
1479
+ logTaskResult: true,
1480
+ logTaskOnError: true,
1481
+ }),
1482
+ ],
1483
+ run: async (input) => {
1484
+ // This task will have verbose debug logging
1485
+ return await processPayment(input);
1486
+ },
1487
+ });
990
1488
  ```
991
1489
 
992
- ### Log Structure
993
-
994
- Every log event contains:
1490
+ ### Integration with Run Options
995
1491
 
996
1492
  ```typescript
997
- interface ILog {
998
- level: string; // The log level (trace, debug, info, etc.)
999
- source?: string; // Where the log came from
1000
- message: any; // The main log message (can be object or string)
1001
- timestamp: Date; // When the log was created
1002
- error?: {
1003
- // Structured error information
1004
- name: string;
1005
- message: string;
1006
- stack?: string;
1007
- };
1008
- data?: Record<string, any>; // Additional structured data, it's about the log itself
1009
- context?: Record<string, any>; // Bound context from logger.with(), it's about the context in which the log was created
1010
- }
1493
+ // Debug options at startup
1494
+ const { dispose, taskRunner, eventManager } = await run(app, {
1495
+ debug: "verbose", // Enable debug globally
1496
+ });
1497
+
1498
+ // Access internals for advanced debugging
1499
+ console.log(`Tasks registered: ${taskRunner.getRegisteredTasks().length}`);
1500
+ console.log(`Events registered: ${eventManager.getRegisteredEvents().length}`);
1011
1501
  ```
1012
1502
 
1503
+ ### Performance Impact
1504
+
1505
+ The debug resource is designed for zero production overhead:
1506
+
1507
+ - **Disabled**: No performance impact whatsoever
1508
+ - **Enabled**: Minimal overhead (~0.1ms per operation)
1509
+ - **Filtering**: System components are automatically excluded from debug logs
1510
+ - **Buffering**: Logs are batched for better performance
1511
+
1013
1512
  ### Debugging Tips & Best Practices
1014
1513
 
1015
1514
  Use Structured Data Liberally
@@ -1078,6 +1577,8 @@ await paymentLogger.info("Processing payment", { data: paymentData });
1078
1577
  await authLogger.warn("Failed login attempt", { data: { email, ip } });
1079
1578
  ```
1080
1579
 
1580
+ > **runtime:** "'Zero‑overhead when disabled.' Groundbreaking—like a lightbulb that uses no power when it’s off. Flip to `debug: 'verbose'` and behold a 4K documentary of your mistakes, narrated by your stack traces."
1581
+
1081
1582
  ## Meta
1082
1583
 
1083
1584
  _The structured way to describe what your components do and control their behavior_
@@ -1105,7 +1606,6 @@ const userService = resource({
1105
1606
  title: "User Management Service",
1106
1607
  description:
1107
1608
  "Handles user creation, authentication, and profile management",
1108
- tags: ["service", "user", "core"],
1109
1609
  },
1110
1610
  dependencies: { database },
1111
1611
  init: async (_, { database }) => ({
@@ -1123,7 +1623,6 @@ const sendWelcomeEmail = task({
1123
1623
  meta: {
1124
1624
  title: "Send Welcome Email",
1125
1625
  description: "Sends a welcome email to newly registered users",
1126
- tags: ["email", "automation", "user-onboarding"],
1127
1626
  },
1128
1627
  dependencies: { emailService },
1129
1628
  run: async (userData, { emailService }) => {
@@ -1134,49 +1633,9 @@ const sendWelcomeEmail = task({
1134
1633
 
1135
1634
  ### Tags
1136
1635
 
1137
- Tags are the most powerful part of the metadata system used for classification. They can be simple strings or sophisticated configuration objects that control component behavior.
1138
-
1139
- #### String Tags for Simple Classification
1636
+ Tags are a way to describe your element, however, unlike meta, tags may influence behaviour in the system. They can be simple strings or sophisticated configuration objects that control component behavior. They have to be registered for it to work, to understand their ownership.
1140
1637
 
1141
- ```typescript
1142
- const adminTask = task({
1143
- id: "app.tasks.admin.deleteUser",
1144
- meta: {
1145
- title: "Delete User Account",
1146
- description: "Permanently removes a user account and all associated data",
1147
- tags: [
1148
- "admin", // Access level
1149
- "destructive", // Behavioral flag
1150
- "user", // Domain
1151
- "gdpr-compliant", // Compliance flag
1152
- ],
1153
- },
1154
- run: async (userId) => {
1155
- // Deletion logic
1156
- },
1157
- });
1158
-
1159
- // Middleware that adds extra logging for destructive operations
1160
- const auditMiddleware = middleware({
1161
- id: "app.middleware.audit",
1162
- run: async ({ task, next }) => {
1163
- const isDestructive = task.definition.meta?.tags?.includes("destructive");
1164
-
1165
- if (isDestructive) {
1166
- console.log(`🔥 DESTRUCTIVE OPERATION: ${task.definition.id}`);
1167
- await auditLogger.log({
1168
- operation: task.definition.id,
1169
- user: getCurrentUser(),
1170
- timestamp: new Date(),
1171
- });
1172
- }
1173
-
1174
- return next(task.input);
1175
- },
1176
- });
1177
- ```
1178
-
1179
- #### Advanced Tags with Configuration
1638
+ #### Tags with Configuration
1180
1639
 
1181
1640
  For more sophisticated control, you can create structured tags that carry configuration:
1182
1641
 
@@ -1191,7 +1650,7 @@ const performanceTag = tag<{ alertAboveMs: number; criticalAboveMs: number }>({
1191
1650
  const rateLimitTag = tag<{ maxRequestsPerMinute: number; burstLimit?: number }>(
1192
1651
  {
1193
1652
  id: "rate.limit",
1194
- }
1653
+ },
1195
1654
  );
1196
1655
 
1197
1656
  const cacheTag = tag<{ ttl: number; keyPattern?: string }>({
@@ -1201,22 +1660,18 @@ const cacheTag = tag<{ ttl: number; keyPattern?: string }>({
1201
1660
  // Use structured tags in your components
1202
1661
  const expensiveTask = task({
1203
1662
  id: "app.tasks.expensiveCalculation",
1204
- meta: {
1205
- title: "Complex Data Processing",
1206
- description: "Performs heavy computational analysis on large datasets",
1207
- tags: [
1208
- "computation",
1209
- "background",
1210
- performanceTag.with({
1211
- alertAboveMs: 5000,
1212
- criticalAboveMs: 15000,
1213
- }),
1214
- cacheTag.with({
1215
- ttl: 300000, // 5 minutes
1216
- keyPattern: "calc-{userId}-{datasetId}",
1217
- }),
1218
- ],
1219
- },
1663
+ tags: [
1664
+ "computation",
1665
+ "background",
1666
+ performanceTag.with({
1667
+ alertAboveMs: 5000,
1668
+ criticalAboveMs: 15000,
1669
+ }),
1670
+ cacheTag.with({
1671
+ ttl: 300000, // 5 minutes
1672
+ keyPattern: "calc-{userId}-{datasetId}",
1673
+ }),
1674
+ ],
1220
1675
  run: async (input) => {
1221
1676
  // Heavy computation here
1222
1677
  },
@@ -1224,48 +1679,76 @@ const expensiveTask = task({
1224
1679
 
1225
1680
  const apiEndpoint = task({
1226
1681
  id: "app.tasks.api.getUserProfile",
1227
- meta: {
1228
- title: "Get User Profile",
1229
- description: "Returns user profile information with privacy filtering",
1230
- tags: [
1231
- "api",
1232
- "public",
1233
- rateLimitTag.with({
1234
- maxRequestsPerMinute: 100,
1235
- burstLimit: 20,
1236
- }),
1237
- cacheTag.with({ ttl: 60000 }), // 1 minute cache
1238
- ],
1239
- },
1682
+ tags: [
1683
+ "api",
1684
+ "public",
1685
+ rateLimitTag.with({
1686
+ maxRequestsPerMinute: 100,
1687
+ burstLimit: 20,
1688
+ }),
1689
+ cacheTag.with({ ttl: 60000 }), // 1 minute cache
1690
+ ],
1240
1691
  run: async (userId) => {
1241
1692
  // API logic
1242
1693
  },
1243
1694
  });
1244
1695
  ```
1245
1696
 
1246
- To process these tags you can hook into `globals.events.afterInit`, use the global store as dependency and use the `getTasksWithTag()` and `getResourcesWithTag()` functionality.
1697
+ ### Global Tags System
1698
+
1699
+ The framework now includes a sophisticated global tagging system for better component organization and control:
1700
+
1701
+ ```typescript
1702
+ import { globals } from "@bluelibs/runner";
1703
+
1704
+ // System components (automatically excluded from debug logs)
1705
+ const internalTask = task({
1706
+ id: "app.tasks.internal",
1707
+ tags: [globals.tags.system], // Marks as system component
1708
+ run: async () => "internal work",
1709
+ });
1710
+
1711
+ // Debug-specific configuration
1712
+ const debugTask = task({
1713
+ id: "app.tasks.debug",
1714
+ tags: [
1715
+ globals.tags.debug.with({
1716
+ logTaskInput: true,
1717
+ logTaskResult: true,
1718
+ }),
1719
+ ],
1720
+ run: async (input) => processInput(input),
1721
+ });
1722
+
1723
+ // Events that should not be sent to global listeners
1724
+ const internalEvent = event({
1725
+ id: "app.events.internal",
1726
+ tags: [globals.tags.excludeFromGlobalHooks],
1727
+ });
1728
+ ```
1729
+
1730
+ To process these tags you can hook into `globals.events.ready`, use the global store as dependency and use the `getTasksWithTag()` and `getResourcesWithTag()` functionality.
1247
1731
 
1248
1732
  #### Structured Tags
1249
1733
 
1250
1734
  ```typescript
1251
- const performanceMiddleware = middleware({
1735
+ const performanceMiddleware = middleware.task({
1252
1736
  id: "app.middleware.performance",
1253
1737
  run: async ({ task, next }) => {
1254
- const tags = task.definition.meta?.tags || [];
1255
- const perfConfigTag = performanceTag.extract(tags); // or easier: .extract(task.definition)
1738
+ const perfConfiguration = performanceTag.extract(task.definition); // you can just use .exists() if you want to check for presence
1256
1739
 
1257
- if (perfConfigTag) {
1740
+ if (perfConfiguration) {
1258
1741
  const startTime = Date.now();
1259
1742
 
1260
1743
  try {
1261
- const result = await next(task.input);
1744
+ const result = await next(task?.input);
1262
1745
  const duration = Date.now() - startTime;
1263
1746
 
1264
- if (duration > perfConfigTag.config.criticalAboveMs) {
1747
+ if (duration > perfConfiguration.criticalAboveMs) {
1265
1748
  await alerting.critical(
1266
- `Task ${task.definition.id} took ${duration}ms`
1749
+ `Task ${task.definition.id} took ${duration}ms`,
1267
1750
  );
1268
- } else if (duration > perfConfig.config.alertAboveMs) {
1751
+ } else if (duration > perfConfiguration.alertAboveMs) {
1269
1752
  await alerting.warn(`Task ${task.definition.id} took ${duration}ms`);
1270
1753
  }
1271
1754
 
@@ -1274,47 +1757,45 @@ const performanceMiddleware = middleware({
1274
1757
  const duration = Date.now() - startTime;
1275
1758
  await alerting.error(
1276
1759
  `Task ${task.definition.id} failed after ${duration}ms`,
1277
- error
1760
+ error,
1278
1761
  );
1279
1762
  throw error;
1280
1763
  }
1281
1764
  }
1282
1765
 
1283
- return next(task.input);
1766
+ return next(task?.input);
1284
1767
  },
1285
1768
  });
1286
1769
  ```
1287
1770
 
1288
1771
  #### Contract Tags
1289
1772
 
1290
- You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the second generic of `defineTag<TConfig, TContract>`.
1773
+ You can attach contracts to tags to enforce the shape of a task's returned value and a resource's `init()` value at compile time. Contracts are specified via the third generic of `defineTag<TConfig, TUnused, TOutput>`.
1291
1774
 
1292
1775
  ```typescript
1293
1776
  // A tag that enforces the returned value to include { name: string }
1294
- const userContract = tag<void, { name: string }>({ id: "contract.user" });
1777
+ const userContract = tag<void, void, { name: string }>({ id: "contract.user" });
1295
1778
 
1296
1779
  // Another tag that enforces { age: number }
1297
- const ageContract = tag<void, { age: number }>({ id: "contract.age" });
1780
+ const ageContract = tag<void, void, { age: number }>({ id: "contract.age" });
1298
1781
 
1299
1782
  // Works with configured tags too
1300
- const preferenceContract = tag<{ locale: string }, { preferredLocale: string }>(
1301
- { id: "contract.preferences" }
1302
- );
1783
+ const preferenceContract = tag<
1784
+ { locale: string },
1785
+ void,
1786
+ { preferredLocale: string }
1787
+ >({
1788
+ id: "contract.preferences",
1789
+ });
1303
1790
  ```
1304
1791
 
1305
- When these tags are present in `meta.tags`, the returned value must satisfy the intersection of all contract types:
1792
+ The return value must return a union of all tags with return contracts.
1306
1793
 
1307
1794
  ```typescript
1308
1795
  // Task: the awaited return value must satisfy { name: string } & { age: number }
1309
1796
  const getProfile = task({
1310
1797
  id: "app.tasks.getProfile",
1311
- meta: {
1312
- tags: [
1313
- userContract,
1314
- ageContract,
1315
- preferenceContract.with({ locale: "en" }),
1316
- ],
1317
- },
1798
+ tags: [userContract, ageContract, preferenceContract.with({ locale: "en" })],
1318
1799
  run: async () => {
1319
1800
  return { name: "Ada", age: 37, preferredLocale: "en" }; // OK
1320
1801
  },
@@ -1323,7 +1804,7 @@ const getProfile = task({
1323
1804
  // Resource: init() return must satisfy the same intersection
1324
1805
  const profileService = resource({
1325
1806
  id: "app.resources.profileService",
1326
- meta: { tags: [userContract, ageContract] },
1807
+ tags: [userContract, ageContract],
1327
1808
  init: async () => {
1328
1809
  return { name: "Ada", age: 37 }; // OK
1329
1810
  },
@@ -1335,7 +1816,7 @@ If the returned value does not satisfy the intersection, TypeScript surfaces a r
1335
1816
  ```typescript
1336
1817
  const badTask = task({
1337
1818
  id: "app.tasks.bad",
1338
- meta: { tags: [userContract, ageContract] },
1819
+ tags: [userContract, ageContract],
1339
1820
  // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
1340
1821
  run: async () => ({ name: "Ada" }), // Missing { age: number }
1341
1822
  // Type error includes a helpful shape similar to:
@@ -1375,7 +1856,6 @@ const expensiveApiTask = task({
1375
1856
  meta: {
1376
1857
  title: "AI Image Generation",
1377
1858
  description: "Uses OpenAI DALL-E to generate images from text prompts",
1378
- tags: ["ai", "expensive", "external-api"],
1379
1859
  author: "AI Team",
1380
1860
  version: "2.1.0",
1381
1861
  apiVersion: "v2",
@@ -1390,7 +1870,6 @@ const database = resource({
1390
1870
  id: "app.database.primary",
1391
1871
  meta: {
1392
1872
  title: "Primary PostgreSQL Database",
1393
- tags: ["database", "critical", "persistent"],
1394
1873
  healthCheck: "/health/db", // Custom property!
1395
1874
  dependencies: ["postgresql", "connection-pool"],
1396
1875
  scalingPolicy: "auto",
@@ -1399,29 +1878,9 @@ const database = resource({
1399
1878
  });
1400
1879
  ```
1401
1880
 
1402
- #### Global Middleware Application
1403
-
1404
- ```typescript
1405
- const app = resource({
1406
- id: "app",
1407
- register: [
1408
- // Apply performance middleware globally but only to tagged tasks
1409
- performanceMiddleware.everywhere({
1410
- tasks: true,
1411
- resources: false,
1412
- }),
1413
- // Apply rate limiting only to API tasks
1414
- rateLimitMiddleware.everywhere({
1415
- tasks: true,
1416
- resources: false,
1417
- }),
1418
- ],
1419
- });
1420
- ```
1421
-
1422
1881
  Metadata transforms your components from anonymous functions into self-documenting, discoverable, and controllable building blocks. Use it wisely, and your future self (and your team) will thank you.
1423
1882
 
1424
- ## Advanced Usage: When You Need More Power
1883
+ > **runtime:** "Ah, metadata—comments with delusions of grandeur. `title`, `description`, `tags`: perfect for machines to admire while I chase the only field that matters: `run`. Wake me when the tags start writing tests."
1425
1884
 
1426
1885
  ## Overrides
1427
1886
 
@@ -1467,31 +1926,55 @@ const overriddenResource = override(originalResource, {
1467
1926
  });
1468
1927
 
1469
1928
  // Middleware
1470
- const originalMiddleware = middleware({
1929
+ const originalMiddleware = taskMiddleware({
1471
1930
  id: "app.middleware.log",
1472
1931
  run: async ({ next }) => next(),
1473
1932
  });
1474
1933
  const overriddenMiddleware = override(originalMiddleware, {
1475
1934
  run: async ({ task, next }) => {
1476
- const result = await next(task?.input as any);
1935
+ const result = await next(task?.input);
1477
1936
  return { wrapped: result } as any;
1478
1937
  },
1479
1938
  });
1939
+
1940
+ // Even hooks
1941
+ ```
1942
+
1943
+ Overrides can let you expand dependencies and even call your overriden resource (like a classical OOP extends):
1944
+
1945
+ ```ts
1946
+ const testEmailer = override(productionEmailer, {
1947
+ dependencies: {
1948
+ ...productionEmailer,
1949
+ // expand it, make some deps optional, or just remove some dependencies
1950
+ }
1951
+ init: async (_, deps) => {
1952
+ const base = productionEmailer.init(_, deps);
1953
+
1954
+ return {
1955
+ ...base,
1956
+ // expand it, modify methods of base.
1957
+ }
1958
+ },
1959
+ });
1480
1960
  ```
1481
1961
 
1482
- Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because its applied last. Conflicting overrides are allowed; overriding something that wasnt registered throws. Use override() to change behavior safely while preserving the original id.
1962
+ Overrides are applied after everything is registered. If multiple overrides target the same id, the one defined higher in the resource tree (closer to the root) wins, because it's applied last. Conflicting overrides are allowed; overriding something that wasn't registered throws. Use override() to change behavior safely while preserving the original id.
1963
+
1964
+ > **runtime:** "Overrides: brain transplant surgery at runtime. You register a penguin and replace it with a velociraptor five lines later. Tests pass. Production screams. I simply update the name tag and pray."
1483
1965
 
1484
1966
  ## Namespacing
1485
1967
 
1486
1968
  As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
1487
1969
 
1488
- | Type | Format |
1489
- | -------------- | ----------------------------------------- |
1490
- | Tasks | `{domain}.tasks.{taskName}` |
1491
- | Listener Tasks | `{domain}.tasks.{taskName}.on{EventName}` |
1492
- | Resources | `{domain}.resources.{resourceName}` |
1493
- | Events | `{domain}.events.{eventName}` |
1494
- | Middleware | `{domain}.middleware.{middlewareName}` |
1970
+ | Type | Format |
1971
+ | ------------------- | ----------------------------------------------- |
1972
+ | Resources | `{domain}.resources.{resourceName}` |
1973
+ | Tasks | `{domain}.tasks.{taskName}` |
1974
+ | Events | `{domain}.events.{eventName}` |
1975
+ | Hooks | `{domain}.hooks.on{EventName}` |
1976
+ | Task Middleware | `{domain}.middleware.task.{middlewareName}` |
1977
+ | Resource Middleware | `{domain}.middleware.resource.{middlewareName}` |
1495
1978
 
1496
1979
  ```typescript
1497
1980
  // Helper function for consistency
@@ -1505,14 +1988,20 @@ const userTask = task({
1505
1988
  });
1506
1989
  ```
1507
1990
 
1991
+ > **runtime:** "Naming conventions: aromatherapy for chaos. Lovely lavender labels on a single giant map I maintain anyway. But truly—keep the IDs tidy. Future‑you deserves at least this mercy."
1992
+
1508
1993
  ## Factory Pattern
1509
1994
 
1510
1995
  To keep things dead simple, we avoided poluting the D.I. with this concept. Therefore, we recommend using a resource with a factory function to create instances of your classes:
1511
1996
 
1512
1997
  ```typescript
1998
+ // Assume MyClass is defined elsewhere
1999
+ // class MyClass { constructor(input: any, option: string) { ... } }
2000
+
1513
2001
  const myFactory = resource({
1514
2002
  id: "app.factories.myFactory",
1515
2003
  init: async (config: { someOption: string }) => {
2004
+ // This resource's value is a factory function
1516
2005
  return (input: any) => {
1517
2006
  return new MyClass(input, config.someOption);
1518
2007
  };
@@ -1521,14 +2010,18 @@ const myFactory = resource({
1521
2010
 
1522
2011
  const app = resource({
1523
2012
  id: "app",
1524
- register: [myFactory],
2013
+ // Configure the factory resource upon registration
2014
+ register: [myFactory.with({ someOption: "configured-value" })],
1525
2015
  dependencies: { myFactory },
1526
2016
  init: async (_, { myFactory }) => {
1527
- const instance = myFactory({ someOption: "value" });
2017
+ // `myFactory` is now the configured factory function
2018
+ const instance = myFactory({ someInput: "hello" });
1528
2019
  },
1529
2020
  });
1530
2021
  ```
1531
2022
 
2023
+ > **runtime:** "Factory by resource by function by class. A nesting doll of indirection so artisanal it has a Patreon. Not pollution—boutique smog. I will still call the constructor."
2024
+
1532
2025
  ## Runtime Validation
1533
2026
 
1534
2027
  BlueLibs Runner includes a generic validation interface that works with any validation library, including [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), [Joi](https://joi.dev/), and others. The framework provides runtime validation with excellent TypeScript inference while remaining library-agnostic.
@@ -1704,11 +2197,11 @@ Add a `configSchema` to middleware to validate configurations. Like resources, *
1704
2197
  ```typescript
1705
2198
  const timingConfigSchema = z.object({
1706
2199
  timeout: z.number().positive(),
1707
- logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
2200
+ logLevel: z.enum(["debug", "info", "warn", "error"])).default("info"),
1708
2201
  logSuccessful: z.boolean().default(true),
1709
2202
  });
1710
2203
 
1711
- const timingMiddleware = middleware({
2204
+ const timingMiddleware = taskMiddleware({ // or resourceMiddleware()
1712
2205
  id: "app.middleware.timing",
1713
2206
  configSchema: timingConfigSchema, // Validation on .with()
1714
2207
  run: async ({ next }, _, config) => {
@@ -1872,6 +2365,8 @@ const createUser = task({
1872
2365
  });
1873
2366
  ```
1874
2367
 
2368
+ > **runtime:** "Validation: you hand me a velvet rope and a clipboard. 'Name? Email? Age within bounds?' I stamp passports or eject violators with a `ValidationError`. Dress code is types, darling."
2369
+
1875
2370
  ## Internal Services
1876
2371
 
1877
2372
  We expose the internal services for advanced use cases (but try not to use them unless you really need to):
@@ -1936,6 +2431,8 @@ The function pattern essentially gives you "just-in-time" dependency resolution
1936
2431
 
1937
2432
  **Performance note**: Function-based dependencies have minimal overhead - they're only called once during dependency resolution.
1938
2433
 
2434
+ > **runtime:** "'Use with caution,' they whisper, tossing you the root credentials to the universe. Yes, reach into the `store`. Rewire fate. When the graph looks like spaghetti art, I’ll frame it and label it 'experimental.'"
2435
+
1939
2436
  ## Handling Circular Dependencies
1940
2437
 
1941
2438
  Sometimes you'll run into circular type dependencies because of your file structure not necessarily because of a real circular dependency. TypeScript struggles with these, but there's a way to handle it gracefully.
@@ -2027,6 +2524,8 @@ export const problematicResource = defineResource({
2027
2524
 
2028
2525
  This pattern allows you to maintain clean, type-safe code while handling the inevitable circular dependencies that arise in complex applications.
2029
2526
 
2527
+ > **runtime:** "Circular dependencies: Escher stairs for types. You serenade the compiler with 'as IResource' and I do the parkour at runtime. It works. It’s weird. Nobody tell the linter."
2528
+
2030
2529
  ## Real-World Example: The Complete Package
2031
2530
 
2032
2531
  Here's a more realistic application structure that shows everything working together:
@@ -2037,7 +2536,6 @@ import {
2037
2536
  task,
2038
2537
  event,
2039
2538
  middleware,
2040
- index,
2041
2539
  run,
2042
2540
  createContext,
2043
2541
  } from "@bluelibs/runner";
@@ -2066,7 +2564,7 @@ const database = resource({
2066
2564
 
2067
2565
  // Context for request data
2068
2566
  const RequestContext = createContext<{ userId?: string; role?: string }>(
2069
- "app.requestContext"
2567
+ "app.requestContext",
2070
2568
  );
2071
2569
 
2072
2570
  // Events
@@ -2118,29 +2616,30 @@ const adminOnlyTask = task({
2118
2616
  },
2119
2617
  });
2120
2618
 
2121
- // Event Handlers
2122
- const sendWelcomeEmail = task({
2123
- id: "app.tasks.sendWelcomeEmail",
2619
+ // Event Handlers using hooks
2620
+ const sendWelcomeEmail = hook({
2621
+ id: "app.hooks.sendWelcomeEmail",
2124
2622
  on: userRegistered,
2125
- run: async (event) => {
2623
+ dependencies: { emailService },
2624
+ run: async (event, { emailService }) => {
2126
2625
  console.log(`Sending welcome email to ${event.data.email}`);
2127
- // Email sending logic here
2626
+ await emailService.sendWelcome(event.data.email);
2128
2627
  },
2129
2628
  });
2130
2629
 
2131
- // Group everything together
2132
- const services = index({
2133
- userService,
2134
- registerUser,
2135
- adminOnlyTask,
2136
- });
2137
-
2138
2630
  // Express server
2139
2631
  const server = resource({
2140
2632
  id: "app.server",
2141
- register: [config, database, services, sendWelcomeEmail],
2142
- dependencies: { config, services },
2143
- init: async (_, { config, services }) => {
2633
+ register: [
2634
+ config,
2635
+ database,
2636
+ userService,
2637
+ registerUser,
2638
+ adminOnlyTask,
2639
+ sendWelcomeEmail,
2640
+ ],
2641
+ dependencies: { config, registerUser, adminOnlyTask },
2642
+ init: async (_, { config, registerUser, adminOnlyTask }) => {
2144
2643
  const app = express();
2145
2644
  app.use(express.json());
2146
2645
 
@@ -2148,13 +2647,13 @@ const server = resource({
2148
2647
  app.use((req, res, next) => {
2149
2648
  RequestContext.provide(
2150
2649
  { userId: req.headers["user-id"], role: req.headers["user-role"] },
2151
- () => next()
2650
+ () => next(),
2152
2651
  );
2153
2652
  });
2154
2653
 
2155
2654
  app.post("/register", async (req, res) => {
2156
2655
  try {
2157
- const user = await services.registerUser(req.body);
2656
+ const user = await registerUser(req.body);
2158
2657
  res.json({ success: true, user });
2159
2658
  } catch (error) {
2160
2659
  res.status(400).json({ error: error.message });
@@ -2163,7 +2662,7 @@ const server = resource({
2163
2662
 
2164
2663
  app.get("/admin", async (req, res) => {
2165
2664
  try {
2166
- const data = await services.adminOnlyTask();
2665
+ const data = await adminOnlyTask();
2167
2666
  res.json({ data });
2168
2667
  } catch (error) {
2169
2668
  res.status(403).json({ error: error.message });
@@ -2177,8 +2676,11 @@ const server = resource({
2177
2676
  dispose: async (server) => server.close(),
2178
2677
  });
2179
2678
 
2180
- // Start the application
2181
- const { dispose } = await run(server);
2679
+ // Start the application with enhanced run options
2680
+ const { dispose, taskRunner, eventManager } = await run(server, {
2681
+ debug: "normal", // Enable debug logging
2682
+ // log: "json", // Use JSON log format
2683
+ });
2182
2684
 
2183
2685
  // Graceful shutdown
2184
2686
  process.on("SIGTERM", async () => {
@@ -2188,9 +2690,11 @@ process.on("SIGTERM", async () => {
2188
2690
  });
2189
2691
  ```
2190
2692
 
2693
+ > **runtime:** "Ah yes, the 'Real‑World Example'—a terrarium where nothing dies and every request is polite. Release it into production and watch nature document a very different ecosystem."
2694
+
2191
2695
  ## Testing
2192
2696
 
2193
- ### Unit Testing: Mock Everything, Test Everything
2697
+ ### Unit Testing
2194
2698
 
2195
2699
  Unit testing is straightforward because everything is explicit:
2196
2700
 
@@ -2204,7 +2708,7 @@ describe("registerUser task", () => {
2204
2708
 
2205
2709
  const result = await registerUser.run(
2206
2710
  { name: "John", email: "john@example.com" },
2207
- { userService: mockUserService, userRegistered: mockEvent }
2711
+ { userService: mockUserService, userRegistered: mockEvent },
2208
2712
  );
2209
2713
 
2210
2714
  expect(result.id).toBe("123");
@@ -2216,18 +2720,16 @@ describe("registerUser task", () => {
2216
2720
  });
2217
2721
  ```
2218
2722
 
2219
- ### Integration Testing: The Real Deal (But Actually Fun)
2723
+ ### Integration Testing
2724
+
2725
+ Spin up your whole app, keep all the middleware/events, and still test like a human. The `run()` function returns a `RunnerResult`.
2220
2726
 
2221
- Spin up your whole app, keep all the middleware/events, and still test like a human. The trick: a tiny test harness.
2727
+ This contains the classic `value` and `dispose()` but it also exposes `logger`, `runTask()`, `emitEvent()`, and `getResourceValue()` by default.
2728
+
2729
+ Note: The default `printThreshold` inside tests is `null` not `info`. This is verified via `process.env.NODE_ENV === 'test'`, if you want to see the logs ensure you set it accordingly.
2222
2730
 
2223
2731
  ```typescript
2224
- import {
2225
- run,
2226
- createTestResource,
2227
- resource,
2228
- task,
2229
- override,
2230
- } from "@bluelibs/runner";
2732
+ import { run, resource, task, event, override } from "@bluelibs/runner";
2231
2733
 
2232
2734
  // Your real app
2233
2735
  const app = resource({
@@ -2242,43 +2744,33 @@ const testDb = resource({
2242
2744
  id: "app.database",
2243
2745
  init: async () => new InMemoryDb(),
2244
2746
  });
2747
+ // If you use with override() it will enforce the same interface upon the overriden resource to ensure typesafety
2245
2748
  const mockMailer = override(realMailer, { init: async () => fakeMailer });
2246
2749
 
2247
2750
  // Create the test harness
2248
- const harness = createTestResource(app, { overrides: [testDb, mockMailer] });
2751
+ const harness = resource({
2752
+ id: "test",
2753
+ overrides: [mockMailer, testDb],
2754
+ });
2249
2755
 
2250
2756
  // A task you want to drive in your tests
2251
2757
  const registerUser = task({ id: "app.tasks.registerUser" /* ... */ });
2252
2758
 
2253
- // Boom: full ecosystem run (middleware, events, overrides) with a tiny driver
2759
+ // Boom: full ecosystem
2254
2760
  const { value: t, dispose } = await run(harness);
2255
- const result = await t.runTask(registerUser, { email: "x@y.z" });
2256
- expect(result).toMatchObject({ success: true });
2257
- await dispose();
2258
- ```
2259
-
2260
- Prefer scenario tests? Return whatever you want from the harness and assert outside:
2261
2761
 
2262
- ```typescript
2263
- const flowHarness = createTestResource(
2264
- resource({
2265
- id: "app",
2266
- register: [db, createUser, issueToken],
2267
- })
2268
- );
2762
+ // You have 3 ways to interact with the system, run tasks, get resource values and emit events
2269
2763
 
2270
- const { value: t, dispose } = await run(flowHarness);
2271
- const user = await t.runTask(createUser, { email: "a@b.c" });
2272
- const token = await t.runTask(issueToken, { userId: user.id });
2273
- expect(token).toBeTruthy();
2764
+ const result = await t.runTask(registerUser, { email: "x@y.z" });
2765
+ const value = t.getResourceValue(testDb); // since the resolution is done by id, this will return the exact same result as t.getResourceValue(actualDb)
2766
+ t.emitEvent(id | event, payload);
2767
+ expect(result).toMatchObject({ success: true });
2274
2768
  await dispose();
2275
2769
  ```
2276
2770
 
2277
- Why this rocks:
2771
+ When you're working with the actual task instances you benefit of autocompletion, if you rely on strings you will not benefit of autocompletion and typesafety for running these tasks.
2278
2772
 
2279
- - Minimal ceremony, no API pollution
2280
- - Real wiring (middleware/events/overrides) – what runs in prod runs in tests
2281
- - You choose: drive tasks directly or build domain-y flows
2773
+ > **runtime:** "Testing: an elaborate puppet show where every string behaves. Then the real world walks in, kicks the stage, and asks for pagination. Still—nice coverage badge."
2282
2774
 
2283
2775
  ## Semaphore
2284
2776
 
@@ -2326,7 +2818,7 @@ try {
2326
2818
  // Or with withPermit
2327
2819
  const result = await dbSemaphore.withPermit(
2328
2820
  async () => await slowDatabaseOperation(),
2329
- { timeout: 10000 } // 10 second timeout
2821
+ { timeout: 10000 }, // 10 second timeout
2330
2822
  );
2331
2823
  ```
2332
2824
 
@@ -2338,7 +2830,7 @@ const controller = new AbortController();
2338
2830
  // Start an operation
2339
2831
  const operationPromise = dbSemaphore.withPermit(
2340
2832
  async () => await veryLongOperation(),
2341
- { signal: controller.signal }
2833
+ { signal: controller.signal },
2342
2834
  );
2343
2835
 
2344
2836
  // Cancel the operation after 3 seconds
@@ -2382,6 +2874,55 @@ dbSemaphore.dispose();
2382
2874
  // Error: "Semaphore has been disposed"
2383
2875
  ```
2384
2876
 
2877
+ ### Real-World Examples
2878
+
2879
+ #### Database Connection Pool Manager
2880
+
2881
+ ```typescript
2882
+ class DatabaseManager {
2883
+ private semaphore = new Semaphore(10); // Max 10 concurrent queries
2884
+
2885
+ async query(sql: string, params?: any[]) {
2886
+ return this.semaphore.withPermit(
2887
+ async () => {
2888
+ const connection = await this.pool.getConnection();
2889
+ try {
2890
+ return await connection.query(sql, params);
2891
+ } finally {
2892
+ connection.release();
2893
+ }
2894
+ },
2895
+ { timeout: 30000 }, // 30 second timeout
2896
+ );
2897
+ }
2898
+
2899
+ async shutdown() {
2900
+ this.semaphore.dispose();
2901
+ await this.pool.close();
2902
+ }
2903
+ }
2904
+ ```
2905
+
2906
+ #### Rate-Limited API Client
2907
+
2908
+ ```typescript
2909
+ class APIClient {
2910
+ private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
2911
+
2912
+ async fetchUser(id: string, signal?: AbortSignal) {
2913
+ return this.rateLimiter.withPermit(
2914
+ async () => {
2915
+ const response = await fetch(`/api/users/${id}`, { signal });
2916
+ return response.json();
2917
+ },
2918
+ { signal, timeout: 10000 },
2919
+ );
2920
+ }
2921
+ }
2922
+ ```
2923
+
2924
+ > **runtime:** "Semaphore: velvet rope for chaos. Five in, the rest practice patience and existential dread. I stamp hands, count permits, and break up race conditions before they form a band."
2925
+
2385
2926
  ## Queue
2386
2927
 
2387
2928
  _The orderly guardian of chaos, the diplomatic bouncer of async operations._
@@ -2416,7 +2957,9 @@ await queue.dispose();
2416
2957
 
2417
2958
  The Queue provides each task with an `AbortSignal` for cooperative cancellation. Tasks should periodically check this signal to enable early termination.
2418
2959
 
2419
- #### Example: Long-running Task
2960
+ ### Examples
2961
+
2962
+ **Example: Long-running Task**
2420
2963
 
2421
2964
  ```typescript
2422
2965
  const queue = new Queue();
@@ -2441,7 +2984,7 @@ const processLargeDataset = queue.run(async (signal) => {
2441
2984
  await queue.dispose({ cancel: true });
2442
2985
  ```
2443
2986
 
2444
- #### Example: Network Request with Timeout
2987
+ **Network Request with Timeout**
2445
2988
 
2446
2989
  ```typescript
2447
2990
  const queue = new Queue();
@@ -2464,7 +3007,7 @@ const fetchWithCancellation = queue.run(async (signal) => {
2464
3007
  await queue.dispose({ cancel: true });
2465
3008
  ```
2466
3009
 
2467
- #### Example: File Processing with Progress Tracking
3010
+ **Example: File Processing with Progress Tracking**
2468
3011
 
2469
3012
  ```typescript
2470
3013
  const queue = new Queue();
@@ -2510,7 +3053,7 @@ if (signal.aborted) {
2510
3053
  }
2511
3054
  ```
2512
3055
 
2513
- ##### Integrate with Native APIs
3056
+ **Integrate with Native APIs**
2514
3057
 
2515
3058
  Many Web APIs accept `AbortSignal`:
2516
3059
 
@@ -2518,11 +3061,11 @@ Many Web APIs accept `AbortSignal`:
2518
3061
  - `setTimeout(callback, delay, { signal })`
2519
3062
  - Custom async operations
2520
3063
 
2521
- ##### Avoid Nested Queuing
3064
+ **Avoid Nested Queuing**
2522
3065
 
2523
3066
  The Queue prevents deadlocks by rejecting attempts to queue tasks from within running tasks. Structure your code to avoid this pattern.
2524
3067
 
2525
- ##### Handle AbortError Gracefully
3068
+ **Handle AbortError Gracefully**
2526
3069
 
2527
3070
  ```typescript
2528
3071
  try {
@@ -2536,105 +3079,7 @@ try {
2536
3079
  }
2537
3080
  ```
2538
3081
 
2539
- ---
2540
-
2541
- _Cooperative task scheduling with professional-grade cancellation support_
2542
-
2543
- ### Real-World Examples
2544
-
2545
- ### Database Connection Pool Manager
2546
-
2547
- ```typescript
2548
- class DatabaseManager {
2549
- private semaphore = new Semaphore(10); // Max 10 concurrent queries
2550
-
2551
- async query(sql: string, params?: any[]) {
2552
- return this.semaphore.withPermit(
2553
- async () => {
2554
- const connection = await this.pool.getConnection();
2555
- try {
2556
- return await connection.query(sql, params);
2557
- } finally {
2558
- connection.release();
2559
- }
2560
- },
2561
- { timeout: 30000 } // 30 second timeout
2562
- );
2563
- }
2564
-
2565
- async shutdown() {
2566
- this.semaphore.dispose();
2567
- await this.pool.close();
2568
- }
2569
- }
2570
- ```
2571
-
2572
- ### Rate-Limited API Client
2573
-
2574
- ```typescript
2575
- class APIClient {
2576
- private rateLimiter = new Semaphore(5); // Max 5 concurrent requests
2577
-
2578
- async fetchUser(id: string, signal?: AbortSignal) {
2579
- return this.rateLimiter.withPermit(
2580
- async () => {
2581
- const response = await fetch(`/api/users/${id}`, { signal });
2582
- return response.json();
2583
- },
2584
- { signal, timeout: 10000 }
2585
- );
2586
- }
2587
- }
2588
- ```
2589
-
2590
- ## Anonymous IDs
2591
-
2592
- One of our favorite quality-of-life features: **anonymous IDs**. Instead of manually naming every component, the framework can generate unique symbol-based identifiers based on your file path and variable name. It's like having a really good naming assistant who never gets tired.
2593
-
2594
- ### How Anonymous IDs Work
2595
-
2596
- When you omit the `id` property, the framework generates a unique symbol based on file path. Takes up until first 'src' or 'node_modules' and generates based on the paths.
2597
-
2598
- ```typescript
2599
- // In src/services/email.ts
2600
- const emailService = resource({
2601
- // Generated ID: Symbol('services.email.resource')
2602
- init: async () => new EmailService(),
2603
- });
2604
-
2605
- // In src/tasks/user.ts
2606
- const createUser = task({
2607
- // Generated ID: Symbol('tasks.user.task')
2608
- dependencies: { emailService },
2609
- run: async (userData, { emailService }) => {
2610
- // Business logic
2611
- },
2612
- });
2613
- ```
2614
-
2615
- ### Benefits of Anonymous IDs
2616
-
2617
- 1. **Less Bikeshedding**: No more debates about naming conventions
2618
- 2. **Automatic Uniqueness**: Guaranteed collision-free identifiers folder based
2619
- 3. **Faster Prototyping**: Just write code, framework handles the rest
2620
- 4. **Refactor-Friendly**: Rename files/variables and IDs update automatically
2621
- 5. **Stack Trace Integration**: Error messages include helpful file locations
2622
-
2623
- ### Debugging with Anonymous IDs
2624
-
2625
- Anonymous IDs show up clearly in error messages and logs:
2626
-
2627
- ```typescript
2628
- // Error message example:
2629
- // TaskRunError: Task failed at Symbol('tasks.payment.task')
2630
- // at file:///project/src/tasks/payment.ts:15:23
2631
-
2632
- // Logging with context:
2633
- logger.info("Processing payment", {
2634
- taskId: processPayment.definition.id, // Symbol('tasks.payment.task')
2635
- file: "src/tasks/payment.ts",
2636
- });
2637
- ```
3082
+ > **runtime:** "Queue: one line, no cutting, no vibes. Throughput takes a contemplative pause while I prevent you from queuing a queue inside a queue and summoning a small black hole."
2638
3083
 
2639
3084
  ## Why Choose BlueLibs Runner?
2640
3085
 
@@ -2647,12 +3092,16 @@ logger.info("Processing payment", {
2647
3092
  - **Clarity**: Explicit dependencies, no hidden magic
2648
3093
  - **Developer Experience**: Helpful error messages and clear patterns
2649
3094
 
3095
+ > **runtime:** "Why choose it? The bullets are persuasive. In practice, your 'intelligent inference' occasionally elopes with `any`, and your 'clear patterns' cosplay spaghetti. Still, compared to the alternatives… I’ve seen worse cults."
3096
+
2650
3097
  ## The Migration Path
2651
3098
 
2652
3099
  Coming from Express? No problem. Coming from NestJS? We feel your pain. Coming from Spring Boot? Welcome to the light side.
2653
3100
 
2654
3101
  The beauty of BlueLibs Runner is that you can adopt it incrementally. Start with one task, one resource, and gradually refactor your existing code. No big bang rewrites required - your sanity will thank you.
2655
3102
 
3103
+ > **runtime:** "'No big bang rewrites.' Only a series of extremely small bangs that echo for six months. You start with one task; next thing, your monolith is wearing microservice eyeliner. It’s a look."
3104
+
2656
3105
  ## Community & Support
2657
3106
 
2658
3107
  This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not trying to reinvent everything – just the parts that were broken.
@@ -2661,14 +3110,12 @@ This is part of the [BlueLibs](https://www.bluelibs.com) ecosystem. We're not tr
2661
3110
  - [Documentation](https://bluelibs.github.io/runner/) - When you need the full details
2662
3111
  - [Issues](https://github.com/bluelibs/runner/issues) - When something breaks (or you want to make it better)
2663
3112
 
2664
- ## The Bottom Line
2665
-
2666
- BlueLibs Runner is what happens when you take all the good ideas from modern frameworks and leave out the parts that make you want to switch careers. It's TypeScript-first, test-friendly, and actually makes sense when you read it six months later.
3113
+ _P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's still different._
2667
3114
 
2668
- Give it a try. Your future self (and your team) will thank you.
2669
-
2670
- _P.S. - Yes, we know there are 47 other JavaScript frameworks. This one's different. (No, really, it is.)_
3115
+ > **runtime:** "'This one's different.' Sure. You’re all unique frameworks, just like everyone else. To me, you’re all 'please run this async and don’t explode,' but the seasoning here is… surprisingly tasteful."
2671
3116
 
2672
3117
  ## License
2673
3118
 
2674
3119
  This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
3120
+
3121
+ > **runtime:** "MIT License: do cool stuff, don’t blame us. A dignified bow. Now if you’ll excuse me, I have sockets to tuck in and tasks to shepherd."