@venizia/ignis-helpers 0.1.1-2 → 0.1.1-4

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 (277) hide show
  1. package/README.md +150 -2931
  2. package/dist/common/redact.d.ts +2 -17
  3. package/dist/common/redact.d.ts.map +1 -1
  4. package/dist/common/redact.js +21 -27
  5. package/dist/common/redact.js.map +1 -1
  6. package/dist/modules/base.d.ts +3 -3
  7. package/dist/modules/base.d.ts.map +1 -1
  8. package/dist/modules/base.js.map +1 -1
  9. package/dist/modules/cron/cron.helper.d.ts +1 -5
  10. package/dist/modules/cron/cron.helper.d.ts.map +1 -1
  11. package/dist/modules/cron/cron.helper.js +1 -5
  12. package/dist/modules/cron/cron.helper.js.map +1 -1
  13. package/dist/modules/env/app-env.d.ts +1 -5
  14. package/dist/modules/env/app-env.d.ts.map +1 -1
  15. package/dist/modules/env/app-env.js +1 -5
  16. package/dist/modules/env/app-env.js.map +1 -1
  17. package/dist/modules/error/index.d.ts +5 -17
  18. package/dist/modules/error/index.d.ts.map +1 -1
  19. package/dist/modules/error/index.js +5 -16
  20. package/dist/modules/error/index.js.map +1 -1
  21. package/dist/modules/error/types.d.ts +3 -7
  22. package/dist/modules/error/types.d.ts.map +1 -1
  23. package/dist/modules/error/types.js +3 -7
  24. package/dist/modules/error/types.js.map +1 -1
  25. package/dist/modules/logger/base/abstract.d.ts +13 -0
  26. package/dist/modules/logger/base/abstract.d.ts.map +1 -0
  27. package/dist/modules/logger/base/abstract.js +8 -0
  28. package/dist/modules/logger/base/abstract.js.map +1 -0
  29. package/dist/modules/logger/base/base.d.ts +30 -0
  30. package/dist/modules/logger/base/base.d.ts.map +1 -0
  31. package/dist/modules/logger/base/base.js +44 -0
  32. package/dist/modules/logger/base/base.js.map +1 -0
  33. package/dist/modules/logger/base/index.d.ts +3 -0
  34. package/dist/modules/logger/base/index.d.ts.map +1 -0
  35. package/dist/modules/logger/base/index.js +19 -0
  36. package/dist/modules/logger/base/index.js.map +1 -0
  37. package/dist/modules/logger/common/constants.d.ts +32 -0
  38. package/dist/modules/logger/common/constants.d.ts.map +1 -0
  39. package/dist/modules/logger/common/constants.js +57 -0
  40. package/dist/modules/logger/common/constants.js.map +1 -0
  41. package/dist/modules/logger/common/index.d.ts +3 -0
  42. package/dist/modules/logger/common/index.d.ts.map +1 -0
  43. package/dist/modules/logger/common/index.js +19 -0
  44. package/dist/modules/logger/common/index.js.map +1 -0
  45. package/dist/modules/logger/common/types.d.ts +21 -0
  46. package/dist/modules/logger/common/types.d.ts.map +1 -0
  47. package/dist/modules/logger/common/types.js +3 -0
  48. package/dist/modules/logger/common/types.js.map +1 -0
  49. package/dist/modules/logger/factory.d.ts +15 -2
  50. package/dist/modules/logger/factory.d.ts.map +1 -1
  51. package/dist/modules/logger/factory.js +87 -3
  52. package/dist/modules/logger/factory.js.map +1 -1
  53. package/dist/modules/logger/formatting/deep-splat.d.ts +12 -0
  54. package/dist/modules/logger/formatting/deep-splat.d.ts.map +1 -0
  55. package/dist/modules/logger/formatting/deep-splat.js +59 -0
  56. package/dist/modules/logger/formatting/deep-splat.js.map +1 -0
  57. package/dist/modules/logger/formatting/index.d.ts.map +1 -0
  58. package/dist/modules/logger/formatting/index.js.map +1 -0
  59. package/dist/modules/logger/hf/common/constants.d.ts +23 -0
  60. package/dist/modules/logger/hf/common/constants.d.ts.map +1 -0
  61. package/dist/modules/logger/hf/common/constants.js +31 -0
  62. package/dist/modules/logger/hf/common/constants.js.map +1 -0
  63. package/dist/modules/logger/hf/common/index.d.ts +3 -0
  64. package/dist/modules/logger/hf/common/index.d.ts.map +1 -0
  65. package/dist/modules/logger/hf/common/index.js +19 -0
  66. package/dist/modules/logger/hf/common/index.js.map +1 -0
  67. package/dist/modules/logger/hf/common/types.d.ts +25 -0
  68. package/dist/modules/logger/hf/common/types.d.ts.map +1 -0
  69. package/dist/modules/logger/hf/common/types.js +3 -0
  70. package/dist/modules/logger/hf/common/types.js.map +1 -0
  71. package/dist/modules/logger/hf/flusher.d.ts +15 -0
  72. package/dist/modules/logger/hf/flusher.d.ts.map +1 -0
  73. package/dist/modules/logger/hf/flusher.js +111 -0
  74. package/dist/modules/logger/hf/flusher.js.map +1 -0
  75. package/dist/modules/logger/hf/index.d.ts +5 -0
  76. package/dist/modules/logger/hf/index.d.ts.map +1 -0
  77. package/dist/modules/logger/hf/index.js +23 -0
  78. package/dist/modules/logger/hf/index.js.map +1 -0
  79. package/dist/modules/logger/hf/logger.d.ts +30 -0
  80. package/dist/modules/logger/hf/logger.d.ts.map +1 -0
  81. package/dist/modules/logger/hf/logger.js +110 -0
  82. package/dist/modules/logger/hf/logger.js.map +1 -0
  83. package/dist/modules/logger/hf/ring.d.ts +3 -0
  84. package/dist/modules/logger/hf/ring.d.ts.map +1 -0
  85. package/dist/modules/logger/hf/ring.js +18 -0
  86. package/dist/modules/logger/hf/ring.js.map +1 -0
  87. package/dist/modules/logger/index.d.ts +4 -6
  88. package/dist/modules/logger/index.d.ts.map +1 -1
  89. package/dist/modules/logger/index.js +4 -6
  90. package/dist/modules/logger/index.js.map +1 -1
  91. package/dist/modules/logger/pino/common/index.d.ts +2 -0
  92. package/dist/modules/logger/pino/common/index.d.ts.map +1 -0
  93. package/dist/modules/logger/pino/common/index.js +18 -0
  94. package/dist/modules/logger/pino/common/index.js.map +1 -0
  95. package/dist/modules/logger/pino/common/types.d.ts +13 -0
  96. package/dist/modules/logger/pino/common/types.d.ts.map +1 -0
  97. package/dist/modules/logger/pino/common/types.js +3 -0
  98. package/dist/modules/logger/pino/common/types.js.map +1 -0
  99. package/dist/modules/logger/pino/define.d.ts +73 -0
  100. package/dist/modules/logger/pino/define.d.ts.map +1 -0
  101. package/dist/modules/logger/pino/define.js +185 -0
  102. package/dist/modules/logger/pino/define.js.map +1 -0
  103. package/dist/modules/logger/pino/index.d.ts +8 -0
  104. package/dist/modules/logger/pino/index.d.ts.map +1 -0
  105. package/dist/modules/logger/pino/index.js +24 -0
  106. package/dist/modules/logger/pino/index.js.map +1 -0
  107. package/dist/modules/logger/pino/logger.d.ts +26 -0
  108. package/dist/modules/logger/pino/logger.d.ts.map +1 -0
  109. package/dist/modules/logger/pino/logger.js +41 -0
  110. package/dist/modules/logger/pino/logger.js.map +1 -0
  111. package/dist/modules/logger/winston/common/index.d.ts +2 -0
  112. package/dist/modules/logger/winston/common/index.d.ts.map +1 -0
  113. package/dist/modules/logger/winston/common/index.js +18 -0
  114. package/dist/modules/logger/winston/common/index.js.map +1 -0
  115. package/dist/modules/logger/winston/common/types.d.ts +47 -0
  116. package/dist/modules/logger/winston/common/types.d.ts.map +1 -0
  117. package/dist/modules/logger/winston/common/types.js +3 -0
  118. package/dist/modules/logger/winston/common/types.js.map +1 -0
  119. package/dist/modules/logger/winston/define.d.ts +25 -0
  120. package/dist/modules/logger/winston/define.d.ts.map +1 -0
  121. package/dist/modules/logger/{default-logger.js → winston/define.js} +87 -49
  122. package/dist/modules/logger/winston/define.js.map +1 -0
  123. package/dist/modules/logger/winston/formatters/deep-splat.d.ts +7 -0
  124. package/dist/modules/logger/winston/formatters/deep-splat.d.ts.map +1 -0
  125. package/dist/modules/logger/winston/formatters/deep-splat.js +23 -0
  126. package/dist/modules/logger/winston/formatters/deep-splat.js.map +1 -0
  127. package/dist/modules/logger/winston/formatters/index.d.ts +2 -0
  128. package/dist/modules/logger/winston/formatters/index.d.ts.map +1 -0
  129. package/dist/modules/logger/winston/formatters/index.js +18 -0
  130. package/dist/modules/logger/winston/formatters/index.js.map +1 -0
  131. package/dist/modules/logger/winston/index.d.ts +6 -0
  132. package/dist/modules/logger/winston/index.d.ts.map +1 -0
  133. package/dist/modules/logger/winston/index.js +29 -0
  134. package/dist/modules/logger/winston/index.js.map +1 -0
  135. package/dist/modules/logger/winston/logger.d.ts +24 -0
  136. package/dist/modules/logger/winston/logger.d.ts.map +1 -0
  137. package/dist/modules/logger/winston/logger.js +39 -0
  138. package/dist/modules/logger/winston/logger.js.map +1 -0
  139. package/dist/modules/logger/{transports → winston/transports}/dgram.transport.d.ts +1 -8
  140. package/dist/modules/logger/winston/transports/dgram.transport.d.ts.map +1 -0
  141. package/dist/modules/logger/{transports → winston/transports}/dgram.transport.js +6 -1
  142. package/dist/modules/logger/winston/transports/dgram.transport.js.map +1 -0
  143. package/dist/modules/logger/winston/transports/index.d.ts.map +1 -0
  144. package/dist/modules/logger/winston/transports/index.js.map +1 -0
  145. package/dist/modules/network/tcp-socket/base-tcp-client.helper.d.ts +1 -5
  146. package/dist/modules/network/tcp-socket/base-tcp-client.helper.d.ts.map +1 -1
  147. package/dist/modules/network/tcp-socket/base-tcp-client.helper.js +1 -5
  148. package/dist/modules/network/tcp-socket/base-tcp-client.helper.js.map +1 -1
  149. package/dist/modules/network/tcp-socket/base-tcp-server.helper.d.ts +1 -9
  150. package/dist/modules/network/tcp-socket/base-tcp-server.helper.d.ts.map +1 -1
  151. package/dist/modules/network/tcp-socket/base-tcp-server.helper.js +4 -13
  152. package/dist/modules/network/tcp-socket/base-tcp-server.helper.js.map +1 -1
  153. package/dist/modules/pool/abstract.d.ts +5 -22
  154. package/dist/modules/pool/abstract.d.ts.map +1 -1
  155. package/dist/modules/pool/abstract.js +5 -22
  156. package/dist/modules/pool/abstract.js.map +1 -1
  157. package/dist/modules/pool/base.d.ts +1 -3
  158. package/dist/modules/pool/base.d.ts.map +1 -1
  159. package/dist/modules/pool/base.js +1 -3
  160. package/dist/modules/pool/base.js.map +1 -1
  161. package/dist/modules/pool/types.d.ts +2 -3
  162. package/dist/modules/pool/types.d.ts.map +1 -1
  163. package/dist/modules/queue/common/hook.d.ts +7 -12
  164. package/dist/modules/queue/common/hook.d.ts.map +1 -1
  165. package/dist/modules/queue/common/hook.js +4 -9
  166. package/dist/modules/queue/common/hook.js.map +1 -1
  167. package/dist/modules/queue/internal/hf/helper.d.ts +3 -7
  168. package/dist/modules/queue/internal/hf/helper.d.ts.map +1 -1
  169. package/dist/modules/queue/internal/hf/helper.js +3 -7
  170. package/dist/modules/queue/internal/hf/helper.js.map +1 -1
  171. package/dist/modules/queue/kafka/admin.d.ts +1 -17
  172. package/dist/modules/queue/kafka/admin.d.ts.map +1 -1
  173. package/dist/modules/queue/kafka/admin.js +1 -17
  174. package/dist/modules/queue/kafka/admin.js.map +1 -1
  175. package/dist/modules/queue/kafka/base.d.ts +2 -6
  176. package/dist/modules/queue/kafka/base.d.ts.map +1 -1
  177. package/dist/modules/queue/kafka/base.js +2 -6
  178. package/dist/modules/queue/kafka/base.js.map +1 -1
  179. package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.d.ts +3 -7
  180. package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.d.ts.map +1 -1
  181. package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.js +3 -7
  182. package/dist/modules/queue/kafka/bundler/platformatic-wasm.plugin.js.map +1 -1
  183. package/dist/modules/queue/kafka/consumer.d.ts +1 -26
  184. package/dist/modules/queue/kafka/consumer.d.ts.map +1 -1
  185. package/dist/modules/queue/kafka/consumer.js +1 -26
  186. package/dist/modules/queue/kafka/consumer.js.map +1 -1
  187. package/dist/modules/queue/kafka/producer.d.ts +3 -23
  188. package/dist/modules/queue/kafka/producer.d.ts.map +1 -1
  189. package/dist/modules/queue/kafka/producer.js +3 -23
  190. package/dist/modules/queue/kafka/producer.js.map +1 -1
  191. package/dist/modules/queue/kafka/schema/registry.d.ts +2 -14
  192. package/dist/modules/queue/kafka/schema/registry.d.ts.map +1 -1
  193. package/dist/modules/queue/kafka/schema/registry.js +2 -14
  194. package/dist/modules/queue/kafka/schema/registry.js.map +1 -1
  195. package/dist/modules/redis/utility.d.ts +4 -10
  196. package/dist/modules/redis/utility.d.ts.map +1 -1
  197. package/dist/modules/redis/utility.js +3 -13
  198. package/dist/modules/redis/utility.js.map +1 -1
  199. package/dist/modules/secrets/base/abstract.helper.d.ts.map +1 -1
  200. package/dist/modules/secrets/base/abstract.helper.js +3 -4
  201. package/dist/modules/secrets/base/abstract.helper.js.map +1 -1
  202. package/dist/modules/secrets/hashicorp/hashicorp.helper.d.ts +1 -5
  203. package/dist/modules/secrets/hashicorp/hashicorp.helper.d.ts.map +1 -1
  204. package/dist/modules/secrets/hashicorp/hashicorp.helper.js +1 -5
  205. package/dist/modules/secrets/hashicorp/hashicorp.helper.js.map +1 -1
  206. package/dist/modules/secrets/index.js +1 -1
  207. package/dist/modules/secrets/index.js.map +1 -1
  208. package/dist/modules/socket/socket-io/server/helper.d.ts +1 -5
  209. package/dist/modules/socket/socket-io/server/helper.d.ts.map +1 -1
  210. package/dist/modules/socket/socket-io/server/helper.js +1 -5
  211. package/dist/modules/socket/socket-io/server/helper.js.map +1 -1
  212. package/dist/modules/socket/websocket/server/helper.d.ts +2 -10
  213. package/dist/modules/socket/websocket/server/helper.d.ts.map +1 -1
  214. package/dist/modules/socket/websocket/server/helper.js +2 -10
  215. package/dist/modules/socket/websocket/server/helper.js.map +1 -1
  216. package/dist/modules/storage/base.d.ts.map +1 -1
  217. package/dist/modules/storage/base.js +3 -7
  218. package/dist/modules/storage/base.js.map +1 -1
  219. package/dist/modules/storage/bun-s3/utility.d.ts.map +1 -1
  220. package/dist/modules/storage/bun-s3/utility.js +1 -4
  221. package/dist/modules/storage/bun-s3/utility.js.map +1 -1
  222. package/dist/modules/uid/helper.d.ts +1 -6
  223. package/dist/modules/uid/helper.d.ts.map +1 -1
  224. package/dist/modules/uid/helper.js +1 -6
  225. package/dist/modules/uid/helper.js.map +1 -1
  226. package/dist/modules/worker-thread/base.d.ts +1 -5
  227. package/dist/modules/worker-thread/base.d.ts.map +1 -1
  228. package/dist/modules/worker-thread/base.js +1 -5
  229. package/dist/modules/worker-thread/base.js.map +1 -1
  230. package/dist/modules/worker-thread/worker-bus.d.ts +1 -5
  231. package/dist/modules/worker-thread/worker-bus.d.ts.map +1 -1
  232. package/dist/modules/worker-thread/worker-bus.js +1 -5
  233. package/dist/modules/worker-thread/worker-bus.js.map +1 -1
  234. package/dist/utilities/module.utility.d.ts +7 -10
  235. package/dist/utilities/module.utility.d.ts.map +1 -1
  236. package/dist/utilities/module.utility.js +9 -12
  237. package/dist/utilities/module.utility.js.map +1 -1
  238. package/dist/utilities/performance.utility.d.ts +2 -2
  239. package/dist/utilities/performance.utility.d.ts.map +1 -1
  240. package/dist/utilities/promise.utility.d.ts +3 -11
  241. package/dist/utilities/promise.utility.d.ts.map +1 -1
  242. package/dist/utilities/promise.utility.js +1 -9
  243. package/dist/utilities/promise.utility.js.map +1 -1
  244. package/dist/utilities/retry.utility.d.ts +4 -22
  245. package/dist/utilities/retry.utility.d.ts.map +1 -1
  246. package/dist/utilities/retry.utility.js +2 -20
  247. package/dist/utilities/retry.utility.js.map +1 -1
  248. package/package.json +62 -22
  249. package/dist/modules/logger/application-logger.d.ts +0 -21
  250. package/dist/modules/logger/application-logger.d.ts.map +0 -1
  251. package/dist/modules/logger/application-logger.js +0 -66
  252. package/dist/modules/logger/application-logger.js.map +0 -1
  253. package/dist/modules/logger/default-logger.d.ts +0 -52
  254. package/dist/modules/logger/default-logger.d.ts.map +0 -1
  255. package/dist/modules/logger/default-logger.js.map +0 -1
  256. package/dist/modules/logger/formatters/deep-splat.d.ts +0 -23
  257. package/dist/modules/logger/formatters/deep-splat.d.ts.map +0 -1
  258. package/dist/modules/logger/formatters/deep-splat.js +0 -88
  259. package/dist/modules/logger/formatters/deep-splat.js.map +0 -1
  260. package/dist/modules/logger/formatters/index.d.ts.map +0 -1
  261. package/dist/modules/logger/formatters/index.js.map +0 -1
  262. package/dist/modules/logger/hf-logger.d.ts +0 -27
  263. package/dist/modules/logger/hf-logger.d.ts.map +0 -1
  264. package/dist/modules/logger/hf-logger.js +0 -82
  265. package/dist/modules/logger/hf-logger.js.map +0 -1
  266. package/dist/modules/logger/transports/dgram.transport.d.ts.map +0 -1
  267. package/dist/modules/logger/transports/dgram.transport.js.map +0 -1
  268. package/dist/modules/logger/transports/index.d.ts.map +0 -1
  269. package/dist/modules/logger/transports/index.js.map +0 -1
  270. package/dist/modules/logger/types.d.ts +0 -16
  271. package/dist/modules/logger/types.d.ts.map +0 -1
  272. package/dist/modules/logger/types.js +0 -30
  273. package/dist/modules/logger/types.js.map +0 -1
  274. /package/dist/modules/logger/{formatters → formatting}/index.d.ts +0 -0
  275. /package/dist/modules/logger/{formatters → formatting}/index.js +0 -0
  276. /package/dist/modules/logger/{transports → winston/transports}/index.d.ts +0 -0
  277. /package/dist/modules/logger/{transports → winston/transports}/index.js +0 -0
package/README.md CHANGED
@@ -1,2968 +1,187 @@
1
1
  <div align="center">
2
2
 
3
- # :fire: IGNIS - @venizia/ignis-helpers
3
+ <br />
4
4
 
5
- **Production-ready infrastructure utilities for the IGNIS Framework**
5
+ # :fire: IGNIS - `@venizia/ignis-helpers`
6
6
 
7
- [![npm](https://img.shields.io/npm/v/@venizia/ignis-helpers.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@venizia/ignis-helpers)
8
- [![License](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](https://opensource.org/licenses/MIT)
9
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ **The infrastructure layer: logging, Redis, queues, storage, crypto, networking.**
10
8
 
11
- Logging, Redis, queues (BullMQ/MQTT/Kafka), storage (MinIO/Disk/Memory), cryptography (AES/RSA/ECDH), networking (TCP/TLS/UDP/HTTP), cron, Socket.IO, WebSocket, UID generation, worker threads, and more. Designed to integrate seamlessly with the IGNIS IoC container.
9
+ [![Docs](https://img.shields.io/badge/Docs-ignis.venizia.ai-2563EB.svg?style=flat-square)](https://ignis.venizia.ai/extensions/helpers/)
10
+ [![npm](https://img.shields.io/npm/v/@venizia/ignis-helpers.svg?style=flat-square&color=cb3837&label=@venizia/ignis-helpers)](https://www.npmjs.com/package/@venizia/ignis-helpers)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](LICENSE.md)
12
+ [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.3-f472b6.svg?style=flat-square&logo=bun&logoColor=white)](https://bun.sh)
13
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
12
14
 
13
- [Installation](#installation) &#8226; [Quick Start](#module-overview) &#8226; [API Reference](#logger) &#8226; [Documentation](https://venizia-ai.github.io/ignis)
15
+ [Documentation](https://ignis.venizia.ai/extensions/helpers/) &#8226;
16
+ [Logger](https://ignis.venizia.ai/extensions/helpers/logger/) &#8226;
17
+ [Core API](https://ignis.venizia.ai/references/) &#8226;
18
+ [Changelog](https://ignis.venizia.ai/changelogs/)
14
19
 
15
20
  </div>
16
21
 
17
- ## Highlights
18
-
19
- | | Feature | |
20
- | :---: | :--- | :--- |
21
- | **1** | **14+ Production Modules** | Logger, Redis, Queue, Storage, Crypto, Network, and more |
22
- | **2** | **Pluggable Architecture** | Install only what you use via optional peer deps |
23
- | **3** | **Sub-Path Imports** | Tree-shake heavy modules with `@venizia/ignis-helpers/bullmq`, `/minio`, `/kafka`, etc. |
24
- | **4** | **Consistent API** | Every helper extends `BaseHelper` with scoped logging |
25
- | **5** | **HfLogger** | Zero-allocation ring buffer logger for hot paths (~100-300ns) |
26
- | **6** | **Snowflake UID** | 70-bit distributed IDs at ~4M/sec/worker |
27
-
28
- ---
29
-
30
- ## At a Glance
31
-
32
- The 5 most common imports:
33
-
34
- ```typescript
35
- import { LoggerFactory, RedisSingleHelper, AES } from '@venizia/ignis-helpers';
36
- import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
37
- import { MinioHelper } from '@venizia/ignis-helpers/minio';
38
- ```
39
-
40
22
  ---
41
23
 
42
- ## Table of Contents
43
-
44
- - [Installation](#installation)
45
- - [Module Overview](#module-overview)
46
- - [Logger](#logger)
47
- - [Logger Class](#logger-class)
48
- - [Log Levels](#log-levels)
49
- - [LoggerFactory Caching](#loggerfactory-caching)
50
- - [Custom Logger](#custom-logger)
51
- - [Transport Types](#transport-types)
52
- - [HfLogger (High-Frequency Logger)](#hflogger-high-frequency-logger)
53
- - [Logger Environment Variables](#logger-environment-variables)
54
- - [Redis](#redis)
55
- - [Single Instance](#single-instance)
56
- - [Cluster](#cluster)
57
- - [String Operations](#string-operations)
58
- - [Hash Operations](#hash-operations)
59
- - [Batch Operations](#batch-operations)
60
- - [Key Operations](#key-operations)
61
- - [RedisJSON Operations](#redisjson-operations)
62
- - [Pub/Sub with Compression](#pubsub-with-compression)
63
- - [Raw Command Execution](#raw-command-execution)
64
- - [Connection Management](#connection-management)
65
- - [Redis Event Lifecycle](#redis-event-lifecycle)
66
- - [Reconnection Behavior](#reconnection-behavior)
67
- - [Queue](#queue)
68
- - [BullMQ](#bullmq)
69
- - [In-Memory Queue](#in-memory-queue)
70
- - [MQTT](#mqtt)
71
- - [Kafka (Experimental)](#kafka-experimental)
72
- - [Storage](#storage)
73
- - [Common Interface (IStorageHelper)](#common-interface-istoragehelper)
74
- - [Security Validation](#security-validation)
75
- - [MinIO (S3-Compatible)](#minio-s3-compatible)
76
- - [Disk](#disk)
77
- - [Memory Storage](#memory-storage)
78
- - [Crypto](#crypto)
79
- - [AES](#aes)
80
- - [RSA](#rsa)
81
- - [ECDH](#ecdh)
82
- - [Crypto Utility (hash)](#crypto-utility-hash)
83
- - [Cron](#cron)
84
- - [Socket.IO](#socketio)
85
- - [Server](#socketio-server)
86
- - [Client](#socketio-client)
87
- - [Authentication Flow](#socketio-authentication-flow)
88
- - [WebSocket (Bun Native)](#websocket-bun-native)
89
- - [Server Setup](#websocket-server-setup)
90
- - [Messaging API](#websocket-messaging-api)
91
- - [Encryption Support](#websocket-encryption-support)
92
- - [WebSocket Authentication Flow](#websocket-authentication-flow)
93
- - [Network](#network)
94
- - [HTTP Clients](#http-clients)
95
- - [TCP](#tcp)
96
- - [UDP](#udp)
97
- - [UID (Snowflake)](#uid-snowflake)
98
- - [Bit Layout](#bit-layout)
99
- - [API Reference](#uid-api-reference)
100
- - [Throughput and Limits](#throughput-and-limits)
101
- - [Environment](#environment)
102
- - [Error](#error)
103
- - [Worker Thread](#worker-thread)
104
- - [Common Utilities](#common-utilities)
105
- - [Date Utility](#date-utility)
106
- - [Parse Utility](#parse-utility)
107
- - [Promise Utility](#promise-utility)
108
- - [Request Utility](#request-utility)
109
- - [Performance Utility](#performance-utility)
110
- - [Module Utility](#module-utility)
111
- - [Common Types](#common-types)
112
- - [Key Patterns](#key-patterns)
113
- - [Integration with IGNIS Core](#integration-with-ignis-core)
114
- - [Environment Variables Reference](#environment-variables-reference)
115
- - [Error Handling Patterns](#error-handling-patterns)
116
- - [Performance Tips](#performance-tips)
117
- - [License](#license)
24
+ The pieces every server needs and nobody wants to write again: a scoped logger, Redis, job queues,
25
+ object storage, crypto, cron, sockets, HTTP clients, distributed IDs, worker threads.
118
26
 
119
- ---
27
+ Every helper extends `BaseHelper`, so it arrives with a scoped `logger` and an `identifier` already
28
+ wired. `@venizia/ignis` depends on this package - install it directly when you want the helpers
29
+ outside the framework, or when you need one of the sub-paths below.
120
30
 
121
- ## Installation
31
+ ## Install
122
32
 
123
33
  ```bash
124
34
  bun add @venizia/ignis-helpers
125
35
  ```
126
36
 
127
- The package has several **optional peer dependencies** that you install only for the modules you need:
128
-
129
- | Peer Dependency | Required For | Install Command |
130
- | -------------------------------- | ---------------------------- | --------------------------------------- |
131
- | `bullmq` | BullMQ queue/worker | `bun add bullmq` |
132
- | `cron` | Cron job scheduling | `bun add cron` |
133
- | `minio` | MinIO/S3 storage | `bun add minio` |
134
- | `mqtt` | MQTT pub/sub | `bun add mqtt` |
135
- | `socket.io` | Socket.IO server | `bun add socket.io` |
136
- | `socket.io-client` | Socket.IO client | `bun add socket.io-client` |
137
- | `@socket.io/redis-adapter` | Socket.IO Redis adapter | `bun add @socket.io/redis-adapter` |
138
- | `@socket.io/redis-emitter` | Socket.IO Redis emitter | `bun add @socket.io/redis-emitter` |
139
- | `axios` | Axios HTTP client | `bun add axios` |
140
- | `@platformatic/kafka` | Kafka producer/consumer | `bun add @platformatic/kafka` |
141
-
142
- **Sub-path imports** are available for tree-shaking heavy optional dependencies:
143
-
144
- ```typescript
145
- import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
146
- import { MQTTClientHelper } from '@venizia/ignis-helpers/mqtt';
147
- import { MinioHelper } from '@venizia/ignis-helpers/minio';
148
- import { SocketIOServerHelper } from '@venizia/ignis-helpers/socket-io';
149
- import { AxiosNetworkRequest } from '@venizia/ignis-helpers/axios';
150
- import { CronHelper } from '@venizia/ignis-helpers/cron';
151
- import { KafkaProducerHelper, KafkaConsumerHelper, KafkaAdminHelper } from '@venizia/ignis-helpers/kafka';
152
- ```
153
-
154
- **Core dependencies** (always installed):
155
-
156
- | Dependency | Purpose |
157
- | ------------------------------------------------------------- | ---------------------------------------- |
158
- | `winston` + `winston-daily-rotate-file` + `winston-transport` | Logger transports |
159
- | `ioredis` | Redis client |
160
- | `dayjs` | Date/time utilities |
161
- | `hono` | HTTP framework types, JSX re-exports |
162
- | `drizzle-orm` | ORM types (used across IGNIS) |
163
- | `lodash` | Utility functions |
164
- | `reflect-metadata` | Decorator metadata |
165
- | `@venizia/ignis-inversion` | IoC container, `@inject`/`@injectable` |
166
-
167
- ---
168
-
169
- ## Module Overview
170
-
171
- | Module | Description |
172
- | -------------------- | --------------------------------------------------------------------------------------------- |
173
- | **Logger** | Winston-based logging with daily file rotation, scoped loggers, JSON/text formats, and UDP transport |
174
- | **HfLogger** | High-frequency zero-allocation ring buffer logger for hot paths (~100--300ns) |
175
- | **Redis** | Single instance and Cluster support via IoRedis with auto-reconnect, pub/sub, JSON commands, and zlib compression |
176
- | **Queue (BullMQ)** | Redis-backed distributed job queue with producer/worker roles |
177
- | **Queue (Internal)** | In-memory generator-based queue with state machine (WAITING/PROCESSING/LOCKED/SETTLED) |
178
- | **Queue (MQTT)** | MQTT client for IoT and lightweight pub/sub messaging |
179
- | **Queue (Kafka)** | Kafka producer, consumer, and admin via `@platformatic/kafka` (experimental) |
180
- | **Storage (MinIO)** | S3-compatible object storage with bucket management and file upload |
181
- | **Storage (Disk)** | Local filesystem storage with the same `IStorageHelper` interface |
182
- | **Storage (Memory)** | In-memory key-value storage helper |
183
- | **Crypto (AES)** | AES-256-CBC and AES-256-GCM encryption/decryption with file support |
184
- | **Crypto (RSA)** | RSA key pair generation and public/private key encryption |
185
- | **Crypto (ECDH)** | ECDH P-256 key exchange with HKDF-derived AES-256-GCM (Web Crypto API) |
186
- | **Cron** | Cron job scheduling with modification and duplication |
187
- | **Socket.IO** | Socket.IO server and client helpers with Redis adapter, auth flow, and room management |
188
- | **WebSocket** | Bun-native WebSocket server with Redis pub/sub, encryption support, room/user management |
189
- | **Network (HTTP)** | HTTP request clients with Axios and native fetch backends |
190
- | **Network (TCP)** | TCP server and client with TLS support, auth flow, and client state tracking |
191
- | **Network (UDP)** | UDP client with multicast support |
192
- | **UID** | Snowflake ID generator (48-10-12 bit layout) with Base62 encoding, ~4M IDs/sec/worker |
193
- | **Environment** | Environment detection (`NODE_ENV`) and prefixed env var management |
194
- | **Error** | Standardized `ApplicationError` with status codes and message codes |
195
- | **Worker Thread** | Worker pool management, worker bus for inter-thread messaging |
196
-
197
- ---
198
-
199
- ## Logger
200
-
201
- Winston-based logging system with console, daily-rotated file, and UDP (dgram) transports.
202
-
203
- ### Logger Class
204
-
205
- The primary logger with scope-based caching and `[Scope]` prefixed output.
206
-
207
- ```typescript
208
- import { Logger, LoggerFactory } from '@venizia/ignis-helpers';
209
-
210
- // Get a logger (cached by scope)
211
- const logger = Logger.get('UserService');
212
- logger.info('User created'); // [UserService] User created
213
- logger.error('Something failed'); // [UserService] Something failed
214
-
215
- // Create a method-scoped child logger
216
- logger.for('createUser').info('Validating input');
217
- // [UserService-createUser] Validating input
218
-
219
- // Using LoggerFactory (array-based scope)
220
- const log = LoggerFactory.getLogger(['OrderService', 'v2']);
221
- log.info('Processing order'); // [OrderService-v2] Processing order
222
-
223
- // Custom Winston logger instance
224
- import { defineCustomLogger } from '@venizia/ignis-helpers';
225
- const customWinston = defineCustomLogger({ /* ... */ });
226
- const customScopedLogger = Logger.get('MyScope', customWinston);
227
- ```
228
-
229
- ### Log Levels
230
-
231
- The logger defines a custom level hierarchy. Lower numeric priority means higher severity:
232
-
233
- | Level | Priority | Description | Color |
234
- | --------- | -------- | ------------------------ | ------- |
235
- | `error` | 0 | Error conditions | Red |
236
- | `alert` | 0 | Alert conditions | Red |
237
- | `emerg` | 0 | Emergency conditions | Red |
238
- | `warn` | 1 | Warning conditions | Yellow |
239
- | `info` | 2 | Informational messages | Green |
240
- | `http` | 3 | HTTP request logging | Magenta |
241
- | `verbose` | 4 | Verbose output | Gray |
242
- | `debug` | 5 | Debug messages (requires `DEBUG=true`) | Blue |
243
- | `silly` | 6 | Most verbose | Gray |
244
-
245
- **Debug level behavior:** The `debug` method is gated at module load time. It only produces output when:
246
-
247
- 1. `DEBUG` env var is truthy, AND
248
- 2. `NODE_ENV` is one of the common environments (`local`, `debug`, `development`, `alpha`, `beta`, `staging`, `production`) or listed in `APP_ENV_EXTRA_LOG_ENVS`.
249
-
250
- This means calling `logger.debug()` in production with `DEBUG=false` has **zero runtime cost** -- the check is pre-computed once at module load.
251
-
252
- ```typescript
253
- // Available log methods on Logger:
254
- logger.debug('msg', ...args); // Only when DEBUG=true
255
- logger.info('msg', ...args);
256
- logger.warn('msg', ...args);
257
- logger.error('msg', ...args);
258
- logger.emerg('msg', ...args);
259
- logger.log('http', 'msg', ...args); // Generic method for any level
260
- ```
261
-
262
- ### LoggerFactory Caching
263
-
264
- `Logger.get(scope)` uses a `Map<string, Logger>` for global caching. The same scope always returns the same Logger instance:
265
-
266
- ```typescript
267
- const a = Logger.get('UserService');
268
- const b = Logger.get('UserService');
269
- console.log(a === b); // true -- same cached instance
270
-
271
- // Method-scoped loggers are also cached
272
- const c = a.for('create');
273
- const d = a.for('create');
274
- console.log(c === d); // true -- cached as 'UserService-create'
275
-
276
- // Custom loggers use a separate cache key ('scope:custom')
277
- const custom = Logger.get('MyScope', customWinstonLogger);
278
- ```
279
-
280
- ### Custom Logger
281
-
282
- Build a fully custom Winston logger with file rotation and UDP transports:
283
-
284
- ```typescript
285
- import {
286
- defineCustomLogger,
287
- defineLogFormatter,
288
- defineJsonLoggerFormatter,
289
- definePrettyLoggerFormatter,
290
- LoggerFormats,
291
- } from '@venizia/ignis-helpers';
292
-
293
- // Create a custom formatter
294
- const myFormatter = defineLogFormatter({
295
- label: 'MyApp',
296
- format: 'json', // 'json' or 'text'
297
- });
298
-
299
- // Or use specific formatters directly
300
- const jsonFormat = defineJsonLoggerFormatter({ label: 'MyApp' });
301
- // Combines: label, timestamp, splat, errors(stack), json, colorize
302
-
303
- const prettyFormat = definePrettyLoggerFormatter({ label: 'MyApp' });
304
- // Combines: simple, label, timestamp, splat, align, colorize, printf, errors(stack)
305
- // Output: "2025-01-01T00:00:00.000Z [MyApp] info: message"
306
-
307
- // Full custom logger with all transport options
308
- const myLogger = defineCustomLogger({
309
- // Optional: override default log levels
310
- logLevels: {
311
- error: 0,
312
- warn: 1,
313
- info: 2,
314
- debug: 5,
315
- },
316
- // Optional: override default colors
317
- logColors: {
318
- error: 'red',
319
- warn: 'yellow',
320
- info: 'green',
321
- debug: 'blue',
322
- },
323
- // Optional: custom formatter (defaults to applicationLogFormatter)
324
- loggerFormatter: myFormatter,
325
- transports: {
326
- info: {
327
- file: {
328
- folder: './logs',
329
- prefix: 'myapp',
330
- frequency: '1h', // Rotation frequency
331
- maxSize: '100m', // Max file size
332
- maxFiles: '5d', // Retention period
333
- datePattern: 'YYYYMMDD_HH',
334
- },
335
- dgram: {
336
- socketOptions: { type: 'udp4' },
337
- host: '127.0.0.1',
338
- port: 9999,
339
- label: 'myapp-info',
340
- levels: ['info', 'warn'], // Only forward these levels via UDP
341
- },
342
- },
343
- error: {
344
- file: {
345
- folder: './logs',
346
- prefix: 'myapp',
347
- },
348
- dgram: {
349
- socketOptions: { type: 'udp4' },
350
- host: '127.0.0.1',
351
- port: 9999,
352
- label: 'myapp-error',
353
- levels: ['error', 'emerg'],
354
- },
355
- },
356
- },
357
- });
358
- ```
359
-
360
- ### Transport Types
361
-
362
- **Console Transport:**
363
- Always active. Level: `debug`. Provides colorized output to stdout.
364
-
365
- **DailyRotateFile Transport:**
366
- Configured per info/error channel. Automatically rotates files based on time and size.
367
-
368
- ```
369
- ./logs/APP-info-20250101_14.log # Rotated hourly by default
370
- ./logs/APP-error-20250101_14.log
371
- ```
372
-
373
- | Option | Default | Description |
374
- | ------------- | -------------- | ---------------------------------------- |
375
- | `frequency` | `1h` | How often to rotate |
376
- | `maxSize` | `100m` | Maximum size before forced rotation |
377
- | `maxFiles` | `5d` | How long to retain rotated files |
378
- | `datePattern` | `YYYYMMDD_HH` | Date format in rotated filenames |
37
+ Nothing heavy comes with it. Every module with a third-party backend declares that backend as an
38
+ **optional peer** - install only the ones you use.
379
39
 
380
- **UDP/Dgram Transport:**
381
- Forwards log messages over UDP to a remote collector. Only activates when host, port, label, and levels are all provided.
40
+ ## Usage
382
41
 
383
42
  ```typescript
384
- // The DgramTransport class extends winston-transport
385
- // It only sends messages matching the configured levels
386
- import { DgramTransport } from '@venizia/ignis-helpers';
387
-
388
- const transport = new DgramTransport({
389
- label: 'my-service',
390
- host: '10.0.0.1',
391
- port: 5140,
392
- levels: ['error', 'warn'], // Only forward these levels
393
- socketOptions: { type: 'udp4' },
394
- });
395
-
396
- // Or use the safe factory (returns null if any required option is missing)
397
- const safeTransport = DgramTransport.fromPartial({
398
- host: process.env.LOG_UDP_HOST, // If undefined, returns null
399
- port: parseInt(process.env.LOG_UDP_PORT ?? '0'),
400
- label: 'my-service',
401
- levels: ['error'],
402
- socketOptions: { type: 'udp4' },
403
- });
404
- ```
43
+ // src/index.ts - the entrypoint, before anything logs
44
+ import { LoggerFactory } from '@venizia/ignis-helpers';
45
+ import { PinoLogger } from '@venizia/ignis-helpers/pino';
405
46
 
406
- The Dgram transport auto-reconnects on socket errors and formats messages as:
407
-
408
- ```
409
- <timestamp> [<label>] <level> <message>
47
+ LoggerFactory.use({ provider: PinoLogger });
410
48
  ```
411
49
 
412
- ### HfLogger (High-Frequency Logger)
413
-
414
- Zero-allocation logger using a pre-allocated `SharedArrayBuffer` ring buffer for latency-critical hot paths (~100--300ns per log). Designed for trading engines, real-time pipelines, and any code path where Winston's overhead is unacceptable.
415
-
416
- **Architecture:**
417
-
418
- - 64K-entry ring buffer (65,536 slots x 256 bytes = 16MB `SharedArrayBuffer`)
419
- - Lock-free writes via atomic increment of write index
420
- - Entries are written in binary format, flushed asynchronously to disk
421
-
422
- **Entry format (256 bytes per entry):**
423
-
424
- | Offset | Size | Field |
425
- | ------- | --------- | -------------------------------------------- |
426
- | 0--7 | 8 bytes | Timestamp (BigInt64, nanosecond precision) |
427
- | 8 | 1 byte | Level (0=debug, 1=info, 2=warn, 3=error, 4=emerg) |
428
- | 9--40 | 32 bytes | Scope (fixed-width, padded) |
429
- | 41--255 | 215 bytes | Message (fixed-width, truncated if longer) |
430
-
431
50
  ```typescript
432
- import { HfLogger, HfLogFlusher } from '@venizia/ignis-helpers';
433
-
434
- // --- Initialization phase (once, before hot path) ---
435
-
436
- // Get a cached logger instance (scope bytes are pre-computed)
437
- const logger = HfLogger.get('OrderEngine');
438
-
439
- // Pre-encode messages at init time (ZERO allocation in hot path)
440
- const MSG_SENT = HfLogger.encodeMessage('Order sent');
441
- const MSG_FILLED = HfLogger.encodeMessage('Order filled');
442
- const MSG_REJECTED = HfLogger.encodeMessage('Order rejected');
443
-
444
- // --- Hot path (~100-300ns per call, zero allocation) ---
445
- logger.log('info', MSG_SENT);
446
- logger.log('info', MSG_FILLED);
447
- logger.log('error', MSG_REJECTED);
448
-
449
- // --- Background flush (separate context / interval) ---
450
- const flusher = new HfLogFlusher();
451
- flusher.start(100); // flush every 100ms
452
-
453
- // Or flush manually
454
- await flusher.flush();
455
- ```
456
-
457
- **HfLogger levels:**
458
-
459
- | Level | Numeric Value |
460
- | ------- | ------------- |
461
- | `debug` | 0 |
462
- | `info` | 1 |
463
- | `warn` | 2 |
464
- | `error` | 3 |
465
- | `emerg` | 4 |
466
-
467
- **Key characteristics:**
468
-
469
- - Scope cache: `Map<string, Uint8Array>` -- scope strings are encoded to fixed 32-byte arrays once
470
- - Message cache: `Map<string, Uint8Array>` -- pre-encoded messages avoid `TextEncoder.encode()` in hot path
471
- - Ring buffer wraps around using bitmask: `writeIndex++ & (BUFFER_SIZE - 1)`
472
- - The flusher reads from flushIndex to writeIndex, decoding each entry back to text
473
-
474
- ### Logger Environment Variables
475
-
476
- | Variable | Default | Description |
477
- | ----------------------------------- | -------------- | ---------------------------------------------- |
478
- | `APP_ENV_LOGGER_FOLDER_PATH` | `./` | Directory for log files |
479
- | `APP_ENV_LOGGER_FORMAT` | `text` | Log format: `json` or `text` |
480
- | `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | File rotation frequency |
481
- | `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Max file size before rotation |
482
- | `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Max days to retain log files |
483
- | `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Date pattern for rotated file names |
484
- | `APP_ENV_LOGGER_DGRAM_HOST` | -- | UDP transport host |
485
- | `APP_ENV_LOGGER_DGRAM_PORT` | -- | UDP transport port |
486
- | `APP_ENV_LOGGER_DGRAM_LABEL` | -- | UDP transport label |
487
- | `APP_ENV_LOGGER_DGRAM_LEVELS` | -- | Comma-separated log levels to forward via UDP |
488
- | `DEBUG` | -- | Set to any truthy value to enable `debug` level output |
489
- | `APP_ENV_EXTRA_LOG_ENVS` | -- | Comma-separated extra `NODE_ENV` values that enable debug |
490
- | `APP_ENV_APPLICATION_NAME` | `APP` | Application name prefix for log files and labels |
491
-
492
- ---
493
-
494
- ## Redis
495
-
496
- Full-featured Redis client wrapper around IoRedis supporting single instances and clusters.
497
-
498
- ### Single Instance
51
+ // anywhere else - provider-agnostic
52
+ import { createRedisHelper, ILogger, LoggerFactory, RedisModes } from '@venizia/ignis-helpers';
499
53
 
500
- ```typescript
501
- import { RedisSingleHelper } from '@venizia/ignis-helpers';
54
+ const logger: ILogger = LoggerFactory.getLogger(['CacheService']);
502
55
 
503
- const redis = new RedisSingleHelper({
504
- name: 'main-redis',
56
+ const redis = createRedisHelper({
57
+ mode: RedisModes.SINGLE,
58
+ name: 'cache',
505
59
  host: '127.0.0.1',
506
60
  port: 6379,
507
- password: 'secret',
508
- database: 0, // Redis DB index (default: 0)
509
- autoConnect: true, // Connect immediately (default: true, false = lazyConnect)
510
- maxRetry: 5, // Max reconnect attempts (-1 = unlimited, 0 = no retry, default: 0)
511
- onReady: ({ name }) => console.log(`${name} ready`),
512
- onError: ({ error }) => console.error(error),
513
- });
514
- ```
515
-
516
- ### Cluster
517
-
518
- ```typescript
519
- import { RedisClusterHelper } from '@venizia/ignis-helpers';
520
-
521
- const cluster = new RedisClusterHelper({
522
- name: 'redis-cluster',
523
- nodes: [
524
- { host: 'node1.redis.example.com', port: 6379 },
525
- { host: 'node2.redis.example.com', port: 6379 },
526
- { host: 'node3.redis.example.com', port: 6379 },
527
- ],
528
- clusterOptions: {
529
- redisOptions: { password: 'secret' },
530
- // scaleReads: 'slave', // Optional: read from replicas
531
- // natMap: { ... }, // If behind NAT/proxy
532
- },
533
- onReady: ({ name }) => console.log(`${name} cluster ready`),
534
- });
535
- ```
536
-
537
- Both `RedisSingleHelper` and `RedisClusterHelper` extend `AbstractRedisHelper`, which provides all data operations. They differ only in how the underlying IoRedis client is created.
538
-
539
- ### String Operations
540
-
541
- ```typescript
542
- // SET -- auto JSON.stringify for objects
543
- await redis.set({ key: 'user:1', value: { name: 'Alice', age: 30 } });
544
- await redis.set({ key: 'user:1', value: 'simple-string' });
545
- await redis.set({ key: 'counter', value: 42, options: { log: true } }); // Enable debug logging
546
-
547
- // GET -- returns raw string
548
- const raw = await redis.get({ key: 'user:1' }); // '{"name":"Alice","age":30}'
549
-
550
- // GET with custom transform
551
- const parsed = await redis.get({
552
- key: 'user:1',
553
- transform: (str: string) => JSON.parse(str),
554
- }); // { name: 'Alice', age: 30 }
555
-
556
- // getString -- alias for get()
557
- const str = await redis.getString({ key: 'token' });
558
-
559
- // getObject -- auto JSON.parse
560
- const user = await redis.getObject({ key: 'user:1' }); // { name: 'Alice', age: 30 }
561
- ```
562
-
563
- ### Hash Operations
564
-
565
- ```typescript
566
- // HSET -- set multiple hash fields at once
567
- await redis.hset({ key: 'hash:1', value: { field1: 'data1', field2: 'data2' } });
568
- // Returns number of NEW fields added
569
-
570
- // HGETALL -- get all fields as object
571
- const all = await redis.hgetall({ key: 'hash:1' });
572
- // { field1: 'data1', field2: 'data2' }
573
-
574
- // HGETALL with transform
575
- const transformed = await redis.hgetall({
576
- key: 'hash:1',
577
- transform: (obj) => Object.entries(obj),
578
- });
579
-
580
- // Aliases (PascalCase)
581
- await redis.hSet({ key: 'hash:1', value: { field: 'data' } });
582
- const result = await redis.hGetAll({ key: 'hash:1' });
583
- ```
584
-
585
- ### Batch Operations
586
-
587
- ```typescript
588
- // MSET -- set multiple keys at once (auto JSON.stringify)
589
- await redis.mset({
590
- payload: [
591
- { key: 'k1', value: { a: 1 } },
592
- { key: 'k2', value: { b: 2 } },
593
- { key: 'k3', value: 'plain-string' },
594
- ],
595
- options: { log: true },
596
- });
597
-
598
- // MGET -- get multiple keys at once
599
- const values = await redis.mget({ keys: ['k1', 'k2', 'k3'] });
600
- // ['{"a":1}', '{"b":2}', '"plain-string"']
601
-
602
- // MGET with transform
603
- const objects = await redis.mget({
604
- keys: ['k1', 'k2'],
605
- transform: (str: string) => JSON.parse(str),
606
- });
607
- // [{ a: 1 }, { b: 2 }]
608
-
609
- // Convenience methods
610
- const strings = await redis.getStrings({ keys: ['k1', 'k2'] });
611
- const objs = await redis.getObjects({ keys: ['k1', 'k2'] });
612
-
613
- // Aliases (PascalCase)
614
- await redis.mSet({ payload: [{ key: 'k', value: 'v' }] });
615
- const vals = await redis.mGet({ keys: ['k'] });
616
- ```
617
-
618
- ### Key Operations
619
-
620
- ```typescript
621
- // KEYS -- find keys by pattern
622
- const keys = await redis.keys({ key: 'user:*' });
623
- // ['user:1', 'user:2', 'user:3']
624
-
625
- // DEL -- delete one or more keys
626
- await redis.del({ keys: ['user:1', 'user:2'] });
627
- // Returns number of keys deleted
628
- ```
629
-
630
- ### RedisJSON Operations
631
-
632
- These operations require the [RedisJSON](https://redis.io/docs/stack/json/) module installed on your Redis server.
633
-
634
- ```typescript
635
- // JSON.SET -- set a JSON document at a path
636
- await redis.jSet({ key: 'doc:1', path: '$', value: { name: 'Alice', age: 30, tags: [] } });
637
- // Returns 'OK'
638
-
639
- // JSON.GET -- retrieve JSON at a path
640
- const doc = await redis.jGet({ key: 'doc:1', path: '$' });
641
- // Returns the JSON value
642
-
643
- // JSON.GET with default path
644
- const full = await redis.jGet({ key: 'doc:1' }); // path defaults to '$'
645
-
646
- // JSON.DEL -- delete a path within a JSON document
647
- await redis.jDelete({ key: 'doc:1', path: '$.age' });
648
- // Returns number of paths deleted
649
-
650
- // JSON.NUMINCRBY -- increment a numeric value
651
- await redis.jNumberIncreaseBy({ key: 'doc:1', path: '$.age', value: 1 });
652
- // Returns new value as string
653
-
654
- // JSON.STRAPPEND -- append to a string value
655
- await redis.jStringAppend({ key: 'doc:1', path: '$.name', value: ' Smith' });
656
- // Returns array of new string lengths
657
-
658
- // JSON.ARRAPPEND -- push to an array
659
- await redis.jPush({ key: 'doc:1', path: '$.tags', value: 'admin' });
660
- // Returns array of new array lengths
661
-
662
- // JSON.ARRPOP -- pop from an array
663
- const popped = await redis.jPop({ key: 'doc:1', path: '$.tags' });
664
- // Returns the popped value
665
- ```
666
-
667
- ### Pub/Sub with Compression
668
-
669
- ```typescript
670
- // Subscribe to a topic
671
- redis.subscribe({ topic: 'events' });
672
-
673
- // Listen for messages on the underlying IoRedis client
674
- redis.client.on('message', (channel: string, message: Buffer | string) => {
675
- // If compressed, decompress:
676
- // const data = JSON.parse(zlib.inflateSync(message).toString());
677
- console.log(channel, message);
678
- });
679
-
680
- // Publish to one or more topics
681
- await redis.publish({
682
- topics: ['events', 'notifications'],
683
- payload: { type: 'user.created', userId: '123' },
684
- });
685
-
686
- // Publish with zlib compression (deflateSync)
687
- await redis.publish({
688
- topics: ['events'],
689
- payload: { type: 'user.created', userId: '123' },
690
- useCompress: true, // Compresses payload with zlib.deflateSync before publishing
691
- });
692
-
693
- // Unsubscribe from a topic
694
- redis.unsubscribe({ topic: 'events' });
695
- ```
696
-
697
- ### Raw Command Execution
698
-
699
- Execute any Redis command directly:
700
-
701
- ```typescript
702
- // Simple command with no parameters
703
- const pong = await redis.execute<string>('PING');
704
- // 'PONG'
705
-
706
- // Command with parameters
707
- const result = await redis.execute<string>('SET', ['mykey', 'myvalue']);
708
-
709
- // Complex command
710
- const info = await redis.execute<string>('INFO', ['server']);
711
-
712
- // Custom module commands
713
- const custom = await redis.execute<any>('FT.SEARCH', ['my-index', '*']);
714
- ```
715
-
716
- ### Connection Management
717
-
718
- ```typescript
719
- // Connect (if using lazyConnect / autoConnect: false)
720
- const connected = await redis.connect();
721
- // Returns true if status is 'ready', false if already connecting/ready
722
-
723
- // Disconnect gracefully (sends QUIT command)
724
- const disconnected = await redis.disconnect();
725
- // Returns true if quit was successful
726
-
727
- // Check connection status
728
- redis.client.status; // 'wait' | 'connecting' | 'connect' | 'ready' | 'close' | 'reconnecting' | 'end'
729
-
730
- // Ping
731
- await redis.ping(); // 'PONG'
732
-
733
- // Access the underlying IoRedis client directly
734
- const ioRedisClient = redis.getClient(); // Redis (single) or Cluster instance
735
- ```
736
-
737
- ### Redis Event Lifecycle
738
-
739
- Events are fired in order during the connection lifecycle:
740
-
741
- | Order | Callback | Triggered When | Receives |
742
- | ----- | --------------- | -------------------------------------- | ---------------------- |
743
- | 1 | `onInitialized` | Helper instance created (synchronous) | `{ name, helper }` |
744
- | 2 | `onConnected` | Redis TCP connection established | `{ name, helper }` |
745
- | 3 | `onReady` | Redis is ready to accept commands | `{ name, helper }` |
746
- | -- | `onError` | Any Redis error | `{ name, helper, error }` |
747
-
748
- Additionally, the client logs `reconnecting` events automatically.
749
-
750
- ### Reconnection Behavior
751
-
752
- The Redis helper (`AbstractRedisHelper`) uses exponential backoff for reconnection:
753
-
754
- ```
755
- Strategy: Math.max(Math.min(attempt * 2000, 5000), 1000) ms
756
-
757
- Attempt 1: 1000ms (min clamp)
758
- Attempt 2: 2000ms
759
- Attempt 3: 4000ms
760
- Attempt 4: 5000ms (max clamp)
761
- Attempt 5: 5000ms (max clamp)
762
- ...continues at 5000ms until maxRetry is reached
763
- ```
764
-
765
- - `maxRetry: 0` -- no retries (default)
766
- - `maxRetry: -1` -- unlimited retries (not recommended for production without monitoring)
767
- - `maxRetry: 5` -- retry 5 times then give up
768
-
769
- The helper also sets `maxRetriesPerRequest: null` (required by BullMQ) and `showFriendlyErrorStack: true`.
770
-
771
- ---
772
-
773
- ## Queue
774
-
775
- ### BullMQ
776
-
777
- Redis-backed distributed job queue using BullMQ. Import from the sub-path to avoid bundling BullMQ when unused.
778
-
779
- **Producer setup:**
780
-
781
- ```typescript
782
- import { BullMQHelper } from '@venizia/ignis-helpers/bullmq';
783
-
784
- const producer = BullMQHelper.newInstance({
785
- queueName: 'email-queue',
786
- identifier: 'email-producer',
787
- role: 'queue', // 'queue' = producer, 'worker' = consumer
788
- redisConnection: redis, // IRedisHelper instance
789
- });
790
-
791
- // Add jobs to the queue
792
- await producer.queue.add('send-welcome', {
793
- to: 'user@example.com',
794
- template: 'welcome',
795
- });
796
-
797
- // Add with BullMQ job options
798
- await producer.queue.add('send-invoice', data, {
799
- delay: 5000, // Delay 5 seconds
800
- attempts: 3, // Retry up to 3 times
801
- backoff: { type: 'exponential', delay: 1000 },
802
- priority: 1, // Lower = higher priority
803
- });
804
-
805
- // Cleanup
806
- await producer.close();
807
- ```
808
-
809
- **Worker setup:**
810
-
811
- ```typescript
812
- const worker = BullMQHelper.newInstance({
813
- queueName: 'email-queue',
814
- identifier: 'email-worker',
815
- role: 'worker',
816
- redisConnection: redis,
817
- numberOfWorker: 3, // Concurrency (default: 1)
818
- lockDuration: 90 * 60 * 1000, // Job lock duration: 90 minutes (default)
819
- onWorkerData: async (job) => {
820
- // Process the job
821
- await sendEmail(job.data);
822
- return { sent: true };
823
- },
824
- onWorkerDataCompleted: async (job, result) => {
825
- console.log(`Job ${job.id} completed`, result);
826
- },
827
- onWorkerDataFail: async (job, error) => {
828
- console.error(`Job ${job?.id} failed`, error);
829
- },
830
- });
831
-
832
- // Cleanup
833
- await worker.close();
834
- ```
835
-
836
- **Default job options (configured on the queue):**
837
-
838
- - `removeOnComplete: true` -- clean up completed jobs automatically
839
- - `removeOnFail: true` -- clean up failed jobs automatically
840
-
841
- **Redis connection note:** BullMQ requires `maxRetriesPerRequest: null`. The Redis helper sets this automatically. The BullMQ helper calls `redisConnection.getClient().duplicate()` for both queue and worker connections so they don't interfere with your main Redis client.
842
-
843
- **Supported roles:**
844
-
845
- - `'queue'` -- creates a `Queue` instance (producer)
846
- - `'worker'` -- creates a `Worker` instance (consumer)
847
-
848
- ### In-Memory Queue
849
-
850
- Generator-based in-memory queue with a state machine for synchronous sequential processing. Useful for ordering operations that must be processed one at a time.
851
-
852
- **State machine:**
853
-
854
- ```
855
- WAITING --(enqueue + autoDispatch)--> PROCESSING --(message done)--> WAITING
856
- | |
857
- +--(lock())--> LOCKED +--(settle request + empty)--> SETTLED
858
- |
859
- +--(unlock())--> WAITING
860
- ```
861
-
862
- | State | Description |
863
- | ---------------- | ---------------------------------------- |
864
- | `000_WAITING` | Idle, waiting for next message |
865
- | `100_PROCESSING` | Currently processing a message |
866
- | `200_LOCKED` | Paused, no new messages processed |
867
- | `300_SETTLED` | Terminal state, no more items accepted |
868
-
869
- ```typescript
870
- import { QueueHelper, QueueStatuses } from '@venizia/ignis-helpers';
871
-
872
- const queue = new QueueHelper<{ userId: string }>({
873
- identifier: 'sync-processor',
874
- autoDispatch: true, // Auto-process on enqueue (default: true)
875
- onMessage: async ({ queueElement }) => {
876
- console.log('Processing:', queueElement.payload);
877
- // { isLocked: true, payload: { userId: 'user-1' } }
878
- },
879
- onDataEnqueue: async ({ identifier, queueElement }) => {
880
- console.log('Enqueued:', queueElement.payload);
881
- },
882
- onDataDequeue: async ({ identifier, queueElement }) => {
883
- console.log('Dequeued:', queueElement.payload);
884
- },
885
- onStateChange: async ({ from, to }) => {
886
- console.log(`State: ${from} -> ${to}`);
887
- },
888
- });
889
-
890
- // Enqueue items (auto-dispatched one at a time)
891
- await queue.enqueue({ userId: 'user-1' });
892
- await queue.enqueue({ userId: 'user-2' });
893
-
894
- // Flow control
895
- queue.lock(); // Pause processing (LOCKED state)
896
- queue.unlock({ shouldProcessNextElement: true }); // Resume + process next
897
- queue.unlock({ shouldProcessNextElement: false }); // Resume without processing
898
-
899
- // Settle -- no more items accepted; transitions to SETTLED when empty
900
- queue.settle();
901
-
902
- // Check state
903
- queue.getState(); // Current state string
904
- queue.isSettled(); // true when SETTLED and storage is empty
905
- queue.getTotalEvent(); // Total items ever enqueued
906
- queue.getProcessingEvents(); // Set of currently processing elements
907
-
908
- // Close -- settle + return generator
909
- queue.close();
910
-
911
- // Manual dispatch (when autoDispatch: false)
912
- queue.nextMessage();
913
- ```
914
-
915
- ### MQTT
916
-
917
- Lightweight MQTT pub/sub client for IoT messaging.
918
-
919
- ```typescript
920
- import { MQTTClientHelper } from '@venizia/ignis-helpers/mqtt';
921
-
922
- const mqtt = new MQTTClientHelper({
923
- identifier: 'iot-client',
924
- url: 'mqtt://broker.example.com:1883',
925
- options: {
926
- username: 'device',
927
- password: 'secret',
928
- clientId: 'sensor-001',
929
- keepalive: 60,
930
- reconnectPeriod: 1000,
931
- },
932
- onConnect: () => console.log('MQTT connected'),
933
- onDisconnect: () => console.log('MQTT disconnected'),
934
- onError: (error) => console.error('MQTT error:', error),
935
- onClose: (error) => console.log('MQTT closed', error),
936
- onMessage: ({ topic, message }) => {
937
- console.log(`[${topic}]`, message.toString());
938
- },
939
- });
940
-
941
- // Subscribe to topics
942
- await mqtt.subscribe({ topics: ['sensors/temperature', 'sensors/humidity'] });
943
-
944
- // Publish a message
945
- await mqtt.publish({ topic: 'commands/led', message: 'ON' });
946
- await mqtt.publish({ topic: 'data/json', message: JSON.stringify({ temp: 23.5 }) });
947
- await mqtt.publish({ topic: 'data/binary', message: Buffer.from([0x01, 0x02]) });
948
- ```
949
-
950
- The MQTT client auto-connects on construction. If the client is already established, `configure()` is a no-op. Subscribe/publish throw `ApplicationError` if the client is not connected.
951
-
952
- ### Kafka (Experimental)
953
-
954
- Apache Kafka helpers built on `@platformatic/kafka`, imported from the `@venizia/ignis-helpers/kafka` sub-path: `KafkaProducerHelper`, `KafkaConsumerHelper`, `KafkaAdminHelper`, and `KafkaSchemaRegistryHelper`, all extending a shared `BaseKafkaHelper` (scoped logging, per-broker health tracking, graceful shutdown, `newInstance()` factory pattern).
955
-
956
- ```typescript
957
- import { KafkaProducerHelper, KafkaAcks } from '@venizia/ignis-helpers/kafka';
958
-
959
- const producer = KafkaProducerHelper.newInstance({
960
- identifier: 'my-producer',
961
- bootstrapBrokers: ['localhost:9092'],
962
- acks: KafkaAcks.ALL, // -1 = all in-sync replicas
963
61
  });
964
62
 
965
- await producer.send({
966
- messages: [{ topic: 'events', key: 'user-1', value: JSON.stringify({ action: 'login' }) }],
967
- });
63
+ await redis.set({ key: 'user:1', value: { name: 'IGNIS' }, options: { expiresIn: 60_000 } });
64
+ const user = await redis.get<{ name: string }>({ key: 'user:1', transform: JSON.parse });
968
65
 
969
- await producer.close();
66
+ logger.for('warmup').info('Cache warmed | user: %j', user);
970
67
  ```
971
68
 
972
- Consumers support consumer groups, lag monitoring, and manual/auto commit; admin covers topic/partition/group/config management. Full API, constants, and examples: [Kafka reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/helpers/kafka/index.md) ([producer](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/helpers/kafka/producer.md), [consumer](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/helpers/kafka/consumer.md), [admin](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/helpers/kafka/admin.md)).
973
-
974
- ---
975
-
976
- ## Storage
69
+ ## Sub-path exports
977
70
 
978
- All storage helpers implement the `IStorageHelper` interface for a unified API across backends.
71
+ **This is the one thing to get right.** A helper that value-imports an optional peer is reachable
72
+ **only** from its own sub-path. The root barrel is peer-free by construction: `import
73
+ '@venizia/ignis-helpers'` works with zero peers installed, and your bundler only ever packages the
74
+ backends you imported by name.
979
75
 
980
- ### Common Interface (IStorageHelper)
76
+ | Sub-path | Exports | Peer dependencies to install |
77
+ | :--- | :--- | :--- |
78
+ | `/winston` | `WinstonLogger` | `winston`, `winston-transport`, `winston-daily-rotate-file` |
79
+ | `/pino` | `PinoLogger` | `pino` (+ `pino-pretty` for text format, `pino-roll` for file rotation) |
80
+ | `/bullmq` | `BullMQHelper` | `bullmq` |
81
+ | `/mqtt` | `MQTTClientHelper` | `mqtt` |
82
+ | `/kafka` | `KafkaProducerHelper`, `KafkaConsumerHelper`, `KafkaAdminHelper`, ... | `@platformatic/kafka` |
83
+ | `/minio` | `MinioHelper` | `minio` |
84
+ | `/bun-s3` | `BunS3Helper` | none - Bun's native S3 client |
85
+ | `/socket-io` | `SocketIOServerHelper`, `SocketIOClientHelper` | `socket.io`, `socket.io-client` (+ `@socket.io/redis-adapter`, `@socket.io/redis-emitter` for multi-node) |
86
+ | `/axios` | `AxiosFetcher`, `AxiosNetworkRequest` | `axios` |
87
+ | `/cron` | `CronHelper` | `cron` |
88
+ | `/hashicorp-vault` | `HashiCorpVaultHelper` | `node-vault` |
89
+ | `/dotenv-vault` | `DotenvVaultHelper` | `@dotenvx/dotenvx` |
981
90
 
982
- ```typescript
983
- interface IStorageHelper {
984
- // Name validation
985
- isValidName(name: string): boolean;
986
-
987
- // Bucket operations
988
- isBucketExists(opts: { name: string }): Promise<boolean>;
989
- getBuckets(): Promise<IBucketInfo[]>;
990
- getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
991
- createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
992
- removeBucket(opts: { name: string }): Promise<boolean>;
993
-
994
- // File operations
995
- upload(opts: {
996
- bucket: string;
997
- files: IUploadFile[];
998
- normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
999
- normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
1000
- }): Promise<IUploadResult[]>;
1001
-
1002
- getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
1003
- getStat(opts: { bucket: string; name: string }): Promise<IFileStat>;
1004
- removeObject(opts: { bucket: string; name: string }): Promise<void>;
1005
- removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
1006
- listObjects(opts: { bucket: string; prefix?: string; useRecursive?: boolean; maxKeys?: number }): Promise<IObjectInfo[]>;
1007
-
1008
- // Utility
1009
- getFileType(opts: { mimeType: string }): string;
1010
- }
1011
- ```
1012
-
1013
- **Related types:**
1014
-
1015
- ```typescript
1016
- interface IUploadFile {
1017
- originalName: string;
1018
- mimetype: string;
1019
- buffer: Buffer;
1020
- size: number;
1021
- encoding?: string;
1022
- folderPath?: string;
1023
- }
1024
-
1025
- interface IUploadResult {
1026
- bucketName: string;
1027
- objectName: string;
1028
- link: string;
1029
- }
1030
-
1031
- interface IFileStat {
1032
- size: number;
1033
- metadata: Record<string, any>;
1034
- lastModified?: Date;
1035
- etag?: string;
1036
- versionId?: string;
1037
- }
1038
-
1039
- interface IBucketInfo {
1040
- name: string;
1041
- creationDate: Date;
1042
- }
1043
-
1044
- interface IObjectInfo {
1045
- name?: string;
1046
- size?: number;
1047
- lastModified?: Date;
1048
- etag?: string;
1049
- prefix?: string;
1050
- }
1051
- ```
1052
-
1053
- ### Security Validation
1054
-
1055
- `isValidName()` is called automatically before bucket and file operations. It prevents:
1056
-
1057
- | Threat | Pattern Blocked | Example |
1058
- | ------------------ | ---------------------------------------------- | ------------------------- |
1059
- | Path traversal | `..`, `/`, `\` | `../../etc/passwd` |
1060
- | Hidden files | Leading `.` | `.env`, `.gitignore` |
1061
- | Shell injection | `;`, `\|`, `&`, `$`, backticks, `<>{}[]!#` | `file; rm -rf /` |
1062
- | Header injection | `\n`, `\r`, `\0` | `file\r\nHeader: inject` |
1063
- | DoS | Length > 255 | Very long filenames |
1064
- | Whitespace-only | Empty or spaces | `" "` |
1065
-
1066
- ```typescript
1067
- storage.isValidName('photo.jpg'); // true
1068
- storage.isValidName('../etc/passwd'); // false -- path traversal
1069
- storage.isValidName('.hidden'); // false -- hidden file
1070
- storage.isValidName('file;rm -rf /'); // false -- shell injection
1071
- storage.isValidName('a'.repeat(300)); // false -- too long
1072
- ```
1073
-
1074
- ### MinIO (S3-Compatible)
1075
-
1076
- ```typescript
1077
- import { MinioHelper } from '@venizia/ignis-helpers/minio';
1078
-
1079
- const storage = new MinioHelper({
1080
- endPoint: 'minio.example.com',
1081
- port: 9000,
1082
- useSSL: false,
1083
- accessKey: 'minioadmin',
1084
- secretKey: 'minioadmin',
1085
- // All minio.ClientOptions are supported
1086
- });
1087
-
1088
- // Bucket operations
1089
- await storage.createBucket({ name: 'uploads' });
1090
- const exists = await storage.isBucketExists({ name: 'uploads' });
1091
- const buckets = await storage.getBuckets(); // IBucketInfo[]
1092
- const bucket = await storage.getBucket({ name: 'uploads' }); // IBucketInfo | null
1093
- await storage.removeBucket({ name: 'empty-bucket' });
1094
-
1095
- // Upload files
1096
- const results = await storage.upload({
1097
- bucket: 'uploads',
1098
- files: [{
1099
- originalName: 'photo.jpg',
1100
- mimetype: 'image/jpeg',
1101
- buffer: fileBuffer,
1102
- size: fileBuffer.length,
1103
- folderPath: 'images', // Optional subfolder
1104
- }],
1105
- // Optional: customize naming
1106
- normalizeNameFn: ({ originalName, folderPath }) =>
1107
- `${folderPath}/${Date.now()}-${originalName}`,
1108
- // Optional: customize link generation
1109
- normalizeLinkFn: ({ bucketName, normalizeName }) =>
1110
- `https://cdn.example.com/${bucketName}/${normalizeName}`,
1111
- });
1112
- // [{ bucketName: 'uploads', objectName: 'images/photo.jpg', link: '/static-assets/uploads/...' }]
1113
-
1114
- // Retrieve file as readable stream
1115
- const stream = await storage.getFile({
1116
- bucket: 'uploads',
1117
- name: 'images/photo.jpg',
1118
- options: {
1119
- versionId: 'abc123', // Optional version
1120
- SSECustomerAlgorithm: 'AES256', // Optional SSE
1121
- SSECustomerKey: 'base64key',
1122
- SSECustomerKeyMD5: 'md5hash',
1123
- },
1124
- });
1125
-
1126
- // Get file metadata
1127
- const stat = await storage.getStat({ bucket: 'uploads', name: 'images/photo.jpg' });
1128
- // { size: 12345, metadata: { ... }, lastModified: Date, etag: '...', versionId: '...' }
1129
-
1130
- // List objects
1131
- const objects = await storage.listObjects({
1132
- bucket: 'uploads',
1133
- prefix: 'images/',
1134
- useRecursive: true,
1135
- maxKeys: 100,
1136
- });
1137
-
1138
- // Delete
1139
- await storage.removeObject({ bucket: 'uploads', name: 'images/photo.jpg' });
1140
- await storage.removeObjects({ bucket: 'uploads', names: ['file1.jpg', 'file2.jpg'] });
1141
-
1142
- // Access underlying minio.Client for advanced operations
1143
- storage.client.presignedGetObject('uploads', 'photo.jpg', 3600); // Pre-signed URL
1144
- ```
1145
-
1146
- ### Disk
1147
-
1148
- Local filesystem storage with the same interface. Buckets are directories; objects are files.
1149
-
1150
- ```typescript
1151
- import { DiskHelper } from '@venizia/ignis-helpers';
1152
-
1153
- const storage = new DiskHelper({
1154
- basePath: './data/storage', // Base directory (created automatically if missing)
1155
- });
1156
-
1157
- // All IStorageHelper operations work identically
1158
- await storage.createBucket({ name: 'uploads' });
1159
- // Creates: ./data/storage/uploads/
1160
-
1161
- await storage.upload({
1162
- bucket: 'uploads',
1163
- files: [{ originalName: 'doc.pdf', mimetype: 'application/pdf', buffer: buf, size: buf.length }],
1164
- });
1165
- // Writes: ./data/storage/uploads/doc.pdf
1166
- // Returns: [{ bucketName: 'uploads', objectName: 'doc.pdf', link: '/static-resources/uploads/doc.pdf' }]
1167
-
1168
- const stream = await storage.getFile({ bucket: 'uploads', name: 'doc.pdf' });
1169
- // Returns: fs.createReadStream(...)
1170
-
1171
- const stat = await storage.getStat({ bucket: 'uploads', name: 'doc.pdf' });
1172
- // { size: 12345, lastModified: Date, metadata: { mimetype: 'application/pdf' } }
1173
-
1174
- // List with prefix and recursion
1175
- const objects = await storage.listObjects({
1176
- bucket: 'uploads',
1177
- prefix: 'reports/',
1178
- useRecursive: true,
1179
- maxKeys: 50,
1180
- });
1181
-
1182
- // Remove bucket (must be empty)
1183
- await storage.removeBucket({ name: 'uploads' });
1184
- ```
1185
-
1186
- The Disk helper auto-detects MIME types from file extensions using a built-in map (20+ common types, falls back to `application/octet-stream`).
1187
-
1188
- ### Memory Storage
1189
-
1190
- Simple in-memory key-value storage (not implementing `IStorageHelper`). Useful for testing, caching, and temporary data.
1191
-
1192
- ```typescript
1193
- import { MemoryStorageHelper } from '@venizia/ignis-helpers';
1194
-
1195
- // Type-safe container
1196
- const cache = MemoryStorageHelper.newInstance<{
1197
- token: string;
1198
- user: { name: string; role: string };
1199
- settings: Record<string, boolean>;
1200
- }>();
1201
-
1202
- cache.set('token', 'abc123');
1203
- cache.set('user', { name: 'Alice', role: 'admin' });
1204
- cache.set('settings', { darkMode: true });
1205
-
1206
- const token = cache.get<string>('token'); // 'abc123'
1207
- const user = cache.get<{ name: string }>('user'); // { name: 'Alice', role: 'admin' }
1208
-
1209
- cache.isBound('token'); // true
1210
- cache.isBound('missing'); // false
1211
-
1212
- cache.keys(); // ['token', 'user', 'settings']
1213
- cache.getContainer(); // Full internal object
1214
-
1215
- cache.clear(); // Reset to empty object
1216
- cache.keys(); // []
1217
- ```
1218
-
1219
- ---
1220
-
1221
- ## Crypto
1222
-
1223
- ### AES
1224
-
1225
- Symmetric encryption with AES-256-CBC or AES-256-GCM.
1226
-
1227
- **Algorithm comparison:**
1228
-
1229
- | Feature | AES-256-CBC | AES-256-GCM |
1230
- | ---------------- | ------------------------------ | ------------------------------------------- |
1231
- | Confidentiality | Yes | Yes |
1232
- | Authentication | No (encrypt-only) | Yes (AEAD -- built-in auth tag) |
1233
- | Auth tag size | N/A | 16 bytes |
1234
- | Performance | Slightly faster | Slightly slower (auth overhead) |
1235
- | Use case | Legacy compatibility | **Recommended** for new code |
1236
- | Encrypted format | `[IV(16B)][ciphertext]` | `[IV(16B)][authTag(16B)][ciphertext]` |
1237
-
1238
- ```typescript
1239
- import { AES } from '@venizia/ignis-helpers';
1240
-
1241
- // Create instance with chosen algorithm
1242
- const aesCbc = AES.withAlgorithm('aes-256-cbc');
1243
- const aesGcm = AES.withAlgorithm('aes-256-gcm'); // Recommended
1244
-
1245
- // Encrypt string
1246
- const encrypted = aesGcm.encrypt({
1247
- message: 'Hello, World!',
1248
- secret: 'my-secret-key', // Auto-padded/truncated to 32 bytes (256 bits)
1249
- });
1250
- // Returns base64-encoded string: [IV][AuthTag][Ciphertext]
1251
-
1252
- // Decrypt string
1253
- const decrypted = aesGcm.decrypt({
1254
- message: encrypted,
1255
- secret: 'my-secret-key',
1256
- });
1257
- // 'Hello, World!'
1258
-
1259
- // File encryption
1260
- const encryptedContent = aesGcm.encryptFile({
1261
- absolutePath: '/path/to/file.txt',
1262
- secret: 'my-secret-key',
1263
- });
1264
- // Returns base64-encoded encrypted file content
1265
-
1266
- const decryptedContent = aesGcm.decryptFile({
1267
- absolutePath: '/path/to/encrypted.txt',
1268
- secret: 'my-secret-key',
1269
- });
1270
- // Returns original file content as string
1271
-
1272
- // Advanced options
1273
- const result = aesGcm.encrypt({
1274
- message: 'data',
1275
- secret: 'key',
1276
- opts: {
1277
- iv: customIV, // Custom IV (default: crypto.randomBytes(16))
1278
- inputEncoding: 'utf-8', // Input encoding (default: 'utf-8')
1279
- outputEncoding: 'base64', // Output encoding (default: 'base64')
1280
- doThrow: false, // If false, returns original message on error (default: true)
1281
- },
1282
- });
1283
- ```
1284
-
1285
- **Key normalization:** The secret key is automatically padded with null bytes (`0x00`) or truncated to exactly 32 bytes (256 bits). You can pass a passphrase of any length.
1286
-
1287
- ### RSA
1288
-
1289
- Asymmetric encryption using RSA with DER key format.
1290
-
1291
- ```typescript
1292
- import { RSA } from '@venizia/ignis-helpers';
1293
-
1294
- const rsa = RSA.withAlgorithm();
1295
-
1296
- // Generate DER key pair
1297
- const { publicKey, privateKey } = rsa.generateDERKeyPair({ modulus: 2048 });
1298
- // publicKey: Buffer (DER, SPKI format)
1299
- // privateKey: Buffer (DER, PKCS8 format)
1300
-
1301
- // Convert to base64 for storage/transmission
1302
- const pubKeyB64 = publicKey.toString('base64');
1303
- const privKeyB64 = privateKey.toString('base64');
1304
-
1305
- // Encrypt with public key
1306
- const encrypted = rsa.encrypt({
1307
- message: 'Secret message',
1308
- secret: pubKeyB64,
1309
- // opts: {
1310
- // inputEncoding: { key: 'base64', message: 'utf-8' }, // defaults
1311
- // outputEncoding: 'base64', // default
1312
- // doThrow: true, // default
1313
- // }
1314
- });
1315
-
1316
- // Decrypt with private key
1317
- const decrypted = rsa.decrypt({
1318
- message: encrypted,
1319
- secret: privKeyB64,
1320
- // opts: {
1321
- // inputEncoding: { key: 'base64', message: 'base64' }, // defaults
1322
- // outputEncoding: 'utf-8', // default
1323
- // doThrow: true, // default
1324
- // }
1325
- });
1326
-
1327
- // Available modulus sizes
1328
- rsa.generateDERKeyPair({ modulus: 1024 }); // Fast, insecure (testing only)
1329
- rsa.generateDERKeyPair({ modulus: 2048 }); // Standard (default)
1330
- rsa.generateDERKeyPair({ modulus: 4096 }); // High security, slower
1331
- ```
1332
-
1333
- ### ECDH
1334
-
1335
- Elliptic Curve Diffie-Hellman key exchange with HKDF-derived AES-256-GCM session keys. Uses the Web Crypto API for cross-platform compatibility (Bun and browsers).
1336
-
1337
- **Algorithm details:**
1338
-
1339
- - Curve: P-256 (NIST)
1340
- - Key derivation: HKDF with SHA-256
1341
- - Cipher: AES-256-GCM with 128-bit auth tag
1342
- - IV: 12 bytes random per encryption
1343
- - Salt: 32 bytes (generated or shared)
1344
- - Info string: `ignis-ecdh-p256-aes-256-gcm-v1` (customizable)
1345
-
1346
- ```typescript
1347
- import { ECDH } from '@venizia/ignis-helpers';
1348
-
1349
- const ecdh = ECDH.withAlgorithm();
1350
- // Or with custom HKDF info string:
1351
- // const ecdh = ECDH.withAlgorithm({ hkdfInfo: 'my-app-v1' });
1352
-
1353
- // --- Key Exchange Flow ---
1354
-
1355
- // 1. Both parties generate key pairs
1356
- const alice = await ecdh.generateKeyPair();
1357
- // { keyPair: CryptoKeyPair, publicKeyB64: 'base64-encoded-raw-public-key' }
1358
-
1359
- const bob = await ecdh.generateKeyPair();
1360
-
1361
- // 2. Exchange public keys (via network, etc.)
1362
- const alicePeerKey = await ecdh.importPublicKey({ rawKeyB64: bob.publicKeyB64 });
1363
- const bobPeerKey = await ecdh.importPublicKey({ rawKeyB64: alice.publicKeyB64 });
1364
-
1365
- // 3. Derive shared AES-256-GCM keys
1366
- const aliceSession = await ecdh.deriveAESKey({
1367
- privateKey: alice.keyPair.privateKey,
1368
- peerPublicKey: alicePeerKey,
1369
- // salt is auto-generated if not provided
1370
- });
1371
- // { key: CryptoKey, salt: 'base64-encoded-salt' }
1372
-
1373
- const bobSession = await ecdh.deriveAESKey({
1374
- privateKey: bob.keyPair.privateKey,
1375
- peerPublicKey: bobPeerKey,
1376
- salt: aliceSession.salt, // MUST use same salt to derive same key
1377
- });
1378
-
1379
- // 4. Encrypt with Alice's derived key
1380
- const payload = await ecdh.encrypt({
1381
- message: 'Hello Bob!',
1382
- secret: aliceSession.key,
1383
- opts: {
1384
- additionalData: 'optional-aad', // Optional authenticated associated data
1385
- },
1386
- });
1387
- // { iv: 'base64-iv', ct: 'base64-ciphertext' }
1388
-
1389
- // 5. Decrypt with Bob's derived key (same shared secret)
1390
- const plaintext = await ecdh.decrypt({
1391
- message: payload,
1392
- secret: bobSession.key,
1393
- opts: {
1394
- additionalData: 'optional-aad', // Must match what was used in encrypt
1395
- },
1396
- });
1397
- // 'Hello Bob!'
1398
- ```
1399
-
1400
- ### Crypto Utility (hash)
1401
-
1402
- Simple hashing utility for HMAC-SHA256 and MD5:
1403
-
1404
- ```typescript
1405
- import { hash } from '@venizia/ignis-helpers';
1406
-
1407
- // HMAC-SHA256 (requires secret)
1408
- const sha256 = hash('data', {
1409
- algorithm: 'SHA256',
1410
- secret: 'hmac-secret',
1411
- outputType: 'hex', // 'hex' | 'base64' | 'latin1'
1412
- });
1413
-
1414
- // MD5 (no secret required)
1415
- const md5 = hash('data', {
1416
- algorithm: 'MD5',
1417
- outputType: 'base64',
1418
- });
1419
-
1420
- // SHA256 without secret returns the original text (no-op)
1421
- const noop = hash('data', { algorithm: 'SHA256', outputType: 'hex' });
1422
- // 'data'
1423
- ```
1424
-
1425
- ---
1426
-
1427
- ## Cron
1428
-
1429
- Scheduled job management built on the `cron` package.
1430
-
1431
- ```typescript
1432
- import { CronHelper } from '@venizia/ignis-helpers/cron';
1433
-
1434
- const job = CronHelper.newInstance({
1435
- cronTime: '0 */5 * * * *', // Every 5 minutes
1436
- onTick: async () => {
1437
- console.log('Running scheduled task');
1438
- },
1439
- autoStart: true,
1440
- tz: 'America/New_York',
1441
- errorHandler: (error) => {
1442
- console.error('Cron error:', error);
1443
- },
1444
- });
1445
-
1446
- // Manual control
1447
- job.start();
1448
-
1449
- // Modify schedule at runtime
1450
- job.modifyCronTime({
1451
- cronTime: '0 */10 * * * *', // Change to every 10 minutes
1452
- shouldFireOnTick: true, // Fire immediately after modification
1453
- });
1454
-
1455
- // Duplicate with different schedule (same onTick handler)
1456
- const nightJob = job.duplicate({
1457
- cronTime: '0 0 2 * * *', // 2:00 AM daily
1458
- });
1459
- nightJob.start();
1460
-
1461
- // Access underlying CronJob instance
1462
- const nextDate = job.instance.nextDate();
1463
- ```
1464
-
1465
- ---
1466
-
1467
- ## Socket.IO
1468
-
1469
- Real-time communication helpers supporting both Node.js and Bun runtimes, with Redis adapter for horizontal scaling.
1470
-
1471
- ### Socket.IO Server
1472
-
1473
- ```typescript
1474
- import { SocketIOServerHelper } from '@venizia/ignis-helpers/socket-io';
1475
-
1476
- const io = new SocketIOServerHelper({
1477
- identifier: 'main-ws',
1478
- runtime: 'node', // or 'bun'
1479
- server: httpServer, // Node.js HTTP server (for 'node' runtime)
1480
- // engine: bunEngine, // @socket.io/bun-engine (for 'bun' runtime)
1481
- serverOptions: {
1482
- cors: { origin: '*' },
1483
- path: '/socket.io',
1484
- },
1485
- redisConnection: redis, // IRedisHelper instance
1486
- defaultRooms: ['io-default', 'io-notification'],
1487
- authenticateTimeout: 10_000, // 10s to authenticate
1488
- pingInterval: 30_000, // 30s ping interval
1489
-
1490
- authenticateFn: (handshake) => {
1491
- const token = handshake.auth?.token;
1492
- return verifyJWT(token); // return true/false
1493
- },
1494
- validateRoomFn: ({ socket, rooms }) => {
1495
- return rooms.filter(r => isAuthorized(socket, r));
1496
- },
1497
- clientConnectedFn: async ({ socket }) => {
1498
- console.log('Client authenticated:', socket.id);
1499
- },
1500
- });
1501
-
1502
- await io.configure();
1503
-
1504
- // Send messages
1505
- io.send({
1506
- destination: 'room-123', // room, client ID, or omit for broadcast
1507
- payload: {
1508
- topic: 'notification',
1509
- data: { message: 'Hello!' },
1510
- },
1511
- });
1512
-
1513
- // Register custom event handlers
1514
- io.on({
1515
- topic: 'custom-event',
1516
- handler: (data) => console.log(data),
1517
- });
1518
-
1519
- // Retrieve connected clients
1520
- const clients = io.getClients();
1521
- const client = io.getClients({ id: 'socket-id-123' });
1522
-
1523
- // Shutdown
1524
- await io.shutdown();
1525
- ```
1526
-
1527
- ### Socket.IO Client
1528
-
1529
- ```typescript
1530
- import { SocketIOClientHelper } from '@venizia/ignis-helpers/socket-io';
1531
-
1532
- const client = new SocketIOClientHelper({
1533
- identifier: 'app-client',
1534
- host: 'http://localhost:3000',
1535
- options: {
1536
- path: '/socket.io',
1537
- extraHeaders: { authorization: 'Bearer token123' },
1538
- },
1539
- onConnected: () => console.log('Connected'),
1540
- onAuthenticated: () => console.log('Authenticated'),
1541
- onDisconnected: (reason) => console.log('Disconnected:', reason),
1542
- });
1543
-
1544
- // Authenticate (triggers server-side authenticateFn)
1545
- client.authenticate();
1546
-
1547
- // Subscribe to events
1548
- client.subscribe({
1549
- event: 'notification',
1550
- handler: (data) => console.log('Notification:', data),
1551
- });
1552
-
1553
- // Subscribe to multiple events
1554
- client.subscribeMany({
1555
- events: {
1556
- 'message': (data) => console.log(data),
1557
- 'alert': (data) => console.warn(data),
1558
- },
1559
- });
1560
-
1561
- // Emit events
1562
- client.emit({ topic: 'chat', data: { text: 'Hello!' } });
1563
-
1564
- // Room management
1565
- client.joinRooms({ rooms: ['room-1', 'room-2'] });
1566
- client.leaveRooms({ rooms: ['room-1'] });
1567
-
1568
- // Cleanup
1569
- client.shutdown();
1570
- ```
1571
-
1572
- ### Socket.IO Authentication Flow
1573
-
1574
- 1. Client connects to Socket.IO server
1575
- 2. Server starts authentication timeout (default 10s)
1576
- 3. Client emits `authenticate` event
1577
- 4. Server calls `authenticateFn` with handshake data
1578
- 5. On success: client joins default rooms, receives `authenticated` event, ping interval starts
1579
- 6. On failure: client receives `unauthenticated` event and is disconnected
1580
- 7. On timeout: client is disconnected
1581
-
1582
- **System events:**
1583
-
1584
- | Event | Direction | Description |
1585
- | ----------------- | ------------------ | ---------------------- |
1586
- | `ping` | Server -> Client | Keepalive |
1587
- | `connection` | Client -> Server | Initial connection |
1588
- | `disconnect` | Bidirectional | Disconnection |
1589
- | `join` | Client -> Server | Join rooms |
1590
- | `leave` | Client -> Server | Leave rooms |
1591
- | `authenticate` | Client -> Server | Start authentication |
1592
- | `authenticated` | Server -> Client | Auth success |
1593
- | `unauthenticated` | Server -> Client | Auth failure |
1594
-
1595
- ---
1596
-
1597
- ## WebSocket (Bun Native)
1598
-
1599
- High-performance WebSocket server using Bun's native WebSocket API with Redis pub/sub for multi-instance scaling. Supports optional end-to-end encryption with an outbound transformer pattern.
1600
-
1601
- ### WebSocket Server Setup
1602
-
1603
- ```typescript
1604
- import { WebSocketServerHelper } from '@venizia/ignis-helpers';
1605
-
1606
- const ws = new WebSocketServerHelper({
1607
- identifier: 'realtime',
1608
- server: bunServer, // Bun server instance (IBunServer)
1609
- redisConnection: redis, // IRedisHelper instance
1610
- path: '/ws', // Default: '/ws'
1611
- authTimeout: 5_000, // 5s to authenticate (default)
1612
- heartbeatInterval: 30_000, // 30s between heartbeats (default)
1613
- heartbeatTimeout: 90_000, // 3x interval, disconnect after 3 missed (default)
1614
- requireEncryption: false, // When true, handshakeFn is required
1615
- encryptedBatchLimit: 10, // Max concurrent encryption operations (default)
1616
- defaultRooms: ['ws-default', 'ws-notification'], // Auto-join on auth
1617
-
1618
- serverOptions: {
1619
- maxPayloadLength: 128 * 1024, // 128KB (default)
1620
- idleTimeout: 60, // seconds (default)
1621
- sendPings: true, // default
1622
- perMessageDeflate: false,
1623
- backpressureLimit: 1024 * 1024, // 1MB
1624
- },
1625
-
1626
- authenticateFn: async (data) => {
1627
- const user = await verifyToken(data.token);
1628
- return user ? { userId: user.id, metadata: { role: user.role } } : null;
1629
- },
1630
- validateRoomFn: ({ userId, rooms }) => {
1631
- return rooms.filter(r => canJoinRoom(userId, r));
1632
- },
1633
- clientConnectedFn: async ({ clientId, userId, metadata }) => {
1634
- console.log(`${userId} connected as ${clientId}`, metadata);
1635
- },
1636
- clientDisconnectedFn: async ({ clientId, userId }) => {
1637
- console.log(`${userId} (${clientId}) disconnected`);
1638
- },
1639
- messageHandler: async ({ clientId, userId, message }) => {
1640
- console.log(`[${clientId}] ${message.event}:`, message.data);
1641
- },
1642
- });
1643
-
1644
- await ws.configure();
1645
-
1646
- // Get the Bun WebSocket handler for server.upgrade()
1647
- const handler = ws.getBunWebSocketHandler();
1648
- // Use in Bun.serve({ websocket: handler, fetch: ... })
1649
- ```
1650
-
1651
- ### WebSocket Messaging API
1652
-
1653
- ```typescript
1654
- // --- High-level API (local + Redis cross-instance) ---
1655
-
1656
- // Broadcast to all authenticated clients
1657
- ws.send({ payload: { topic: 'update', data: { version: 2 } } });
1658
-
1659
- // Send to specific client
1660
- ws.send({ destination: clientId, payload: { topic: 'dm', data: { text: 'hi' } } });
1661
-
1662
- // Send to room
1663
- ws.send({ destination: 'room-1', payload: { topic: 'chat', data: { msg: 'hello' } } });
1664
-
1665
- // --- Low-level APIs (local instance only) ---
1666
-
1667
- ws.sendToClient({ clientId, event: 'ping', data: {} });
1668
- ws.sendToUser({ userId: 'user-1', event: 'alert', data: { msg: 'hi' } });
1669
- ws.sendToRoom({ room: 'lobby', event: 'join', data: { user: 'Alice' } });
1670
- ws.sendToRoom({ room: 'lobby', event: 'msg', data: { text: 'hi' }, exclude: [clientId] });
1671
- ws.broadcast({ event: 'system', data: { maintenance: true } });
1672
- ws.broadcast({ event: 'system', data: { maintenance: true }, exclude: ['client-1'] });
1673
-
1674
- // --- Room Management ---
1675
-
1676
- ws.joinRoom({ clientId, room: 'my-room' });
1677
- ws.leaveRoom({ clientId, room: 'my-room' });
1678
-
1679
- // --- Query ---
1680
-
1681
- const allClients = ws.getClients(); // Map<string, IWebSocketClient>
1682
- const oneClient = ws.getClients({ id: clientId }); // IWebSocketClient | undefined
1683
- const userClients = ws.getClientsByUser({ userId: 'user-1' }); // IWebSocketClient[]
1684
- const roomClients = ws.getClientsByRoom({ room: 'lobby' }); // IWebSocketClient[]
1685
-
1686
- await ws.shutdown();
1687
- ```
1688
-
1689
- ### WebSocket Encryption Support
1690
-
1691
- When `requireEncryption: true`, clients must complete an encryption handshake during authentication:
1692
-
1693
- ```typescript
1694
- const ws = new WebSocketServerHelper({
1695
- // ...
1696
- requireEncryption: true,
1697
- handshakeFn: async ({ clientId, userId, data }) => {
1698
- // Generate ECDH key pair for this client
1699
- const keyPair = await ecdh.generateKeyPair();
1700
- // Store privateKey for this client session...
1701
- return {
1702
- serverPublicKey: keyPair.publicKeyB64,
1703
- salt: someSalt,
1704
- };
1705
- // Return null/false to reject the handshake
1706
- },
1707
- outboundTransformer: async ({ client, event, data }) => {
1708
- // Transform outbound messages for encrypted clients
1709
- const encrypted = await encrypt(data, client.metadata.sessionKey);
1710
- return { event, data: encrypted };
1711
- // Return null to send unmodified { event, data }
1712
- },
1713
- });
1714
- ```
1715
-
1716
- When a client has encryption enabled:
1717
-
1718
- - It is unsubscribed from Bun's native pub/sub topics (prevents double delivery)
1719
- - All messages go through the `outboundTransformer` before `socket.send()`
1720
- - Room/broadcast messages are delivered individually with concurrency limit (`encryptedBatchLimit`)
1721
-
1722
- ### WebSocket Authentication Flow
1723
-
1724
- 1. Client connects via `server.upgrade()` -- state: `UNAUTHORIZED`
1725
- 2. Auth timeout starts (default 5s)
1726
- 3. Client sends `{ event: 'authenticate', data: { token: '...' } }`
1727
- 4. Server calls `authenticateFn(data)` -- state: `AUTHENTICATING`
1728
- 5. On success:
1729
- - Client state: `AUTHENTICATED`
1730
- - Subscribes to broadcast topic
1731
- - Joins default rooms + own room
1732
- - If `requireEncryption`: runs `handshakeFn`, enables encryption
1733
- - Sends `connected` event with `{ id, userId, time, serverPublicKey?, salt? }`
1734
- - Calls `clientConnectedFn`
1735
- 6. On failure: sends `error` event, closes with code `4003`
1736
- 7. On timeout: closes with code `4001`
1737
-
1738
- **WebSocket close codes:**
1739
-
1740
- | Code | Reason |
1741
- | ------ | ---------------------------------------- |
1742
- | `1001` | Server shutting down |
1743
- | `4001` | Authentication timeout |
1744
- | `4002` | Heartbeat timeout |
1745
- | `4003` | Authentication failed |
1746
- | `4004` | Encryption required (handshake failed) |
1747
-
1748
- **Redis pub/sub channels:**
1749
-
1750
- | Channel | Purpose |
1751
- | ------------------ | ---------------------------------------- |
1752
- | `ws:broadcast` | Broadcast messages across instances |
1753
- | `ws:room:<name>` | Room-targeted messages |
1754
- | `ws:client:<id>` | Client-targeted messages |
1755
- | `ws:user:<id>` | User-targeted messages |
1756
-
1757
- ---
1758
-
1759
- ## Network
1760
-
1761
- ### HTTP Clients
1762
-
1763
- Two HTTP client implementations sharing a common `IFetchable` interface.
1764
-
1765
- **Common interface:**
1766
-
1767
- ```typescript
1768
- interface IFetchable<V, RQ, RS> {
1769
- send(opts: RQ): Promise<RS>;
1770
- get(opts: RQ): Promise<RS>;
1771
- post(opts: RQ): Promise<RS>;
1772
- put(opts: RQ): Promise<RS>;
1773
- patch(opts: RQ): Promise<RS>;
1774
- delete(opts: RQ): Promise<RS>;
1775
- getWorker(): AxiosInstance | typeof fetch;
1776
- }
1777
- ```
1778
-
1779
- **Node Fetch (built-in, zero dependencies):**
1780
-
1781
- ```typescript
1782
- import { NodeFetchNetworkRequest } from '@venizia/ignis-helpers';
1783
-
1784
- const api = new NodeFetchNetworkRequest({
1785
- name: 'github-api',
1786
- networkOptions: {
1787
- baseUrl: 'https://api.github.com',
1788
- headers: { Authorization: 'token ghp_xxx' },
1789
- // All RequestInit options are supported
1790
- },
1791
- });
1792
-
1793
- const url = api.getRequestUrl({ paths: ['/repos', '/VENIZIA-AI', '/ignis'] });
1794
- // 'https://api.github.com/repos/VENIZIA-AI/ignis'
1795
-
1796
- const response = await api.getNetworkService().get({ url, timeout: 5000 });
1797
- const data = await response.json();
1798
-
1799
- // With query parameters
1800
- const searchUrl = api.getRequestUrl({ paths: ['/search', '/repositories'] });
1801
- const results = await api.getNetworkService().get({
1802
- url: searchUrl,
1803
- params: { q: 'ignis', sort: 'stars' },
1804
- });
1805
- // Fetches: https://api.github.com/search/repositories?q=ignis&sort=stars
1806
-
1807
- // POST with body
1808
- await api.getNetworkService().post({
1809
- url: api.getRequestUrl({ paths: ['/repos'] }),
1810
- body: JSON.stringify({ name: 'new-repo' }),
1811
- headers: { 'Content-Type': 'application/json' },
1812
- });
1813
-
1814
- // Default content-type: 'application/json; charset=utf-8'
1815
- // Timeout implemented via AbortController
1816
- ```
1817
-
1818
- **Axios (requires `bun add axios`):**
1819
-
1820
- ```typescript
1821
- import { AxiosNetworkRequest } from '@venizia/ignis-helpers/axios';
1822
-
1823
- const api = new AxiosNetworkRequest({
1824
- name: 'payment-api',
1825
- networkOptions: {
1826
- baseUrl: 'https://api.payment.com',
1827
- timeout: 30_000, // Default: 60_000 (60s)
1828
- headers: { 'X-API-Key': 'key123' },
1829
- // All AxiosRequestConfig options are supported
1830
- },
1831
- });
1832
-
1833
- const url = api.getRequestUrl({ paths: ['/v1', '/charges'] });
1834
- const response = await api.getNetworkService().post({
1835
- url,
1836
- body: { amount: 1000, currency: 'USD' },
1837
- });
1838
- // response.data contains parsed JSON
1839
-
1840
- // Access the underlying Axios instance for interceptors
1841
- const axiosInstance = api.getWorker();
1842
- axiosInstance.interceptors.request.use((config) => {
1843
- config.headers['X-Request-ID'] = crypto.randomUUID();
1844
- return config;
1845
- });
1846
- axiosInstance.interceptors.response.use(
1847
- (response) => response,
1848
- (error) => {
1849
- if (error.response?.status === 401) {
1850
- // Handle token refresh
1851
- }
1852
- return Promise.reject(error);
1853
- },
1854
- );
1855
-
1856
- // Default configs:
1857
- // withCredentials: true
1858
- // validateStatus: (status) => status < 500
1859
- // HTTPS: auto-creates https.Agent with rejectUnauthorized: false
1860
- ```
1861
-
1862
- **Differences between implementations:**
1863
-
1864
- | Feature | NodeFetch | Axios |
1865
- | ------------- | ----------------------------- | -------------------------------- |
1866
- | Return type | `Response` (Web API) | `AxiosResponse.data` (parsed) |
1867
- | Dependencies | None (built-in `fetch`) | `axios` |
1868
- | Timeout | `AbortController` | Axios built-in |
1869
- | Interceptors | Manual | Built-in request/response |
1870
- | HTTPS | Standard | Auto `https.Agent` |
1871
- | Credentials | Manual headers | `withCredentials: true` |
1872
-
1873
- ### TCP
1874
-
1875
- TCP server and client with optional TLS, authentication flow, and client state tracking.
1876
-
1877
- **Server:**
1878
-
1879
- ```typescript
1880
- import { NetworkTcpServer, NetworkTcpClient } from '@venizia/ignis-helpers';
1881
-
1882
- const server = new NetworkTcpServer({
1883
- identifier: 'data-server',
1884
- serverOptions: {}, // net.ServerOpts
1885
- listenOptions: { host: '0.0.0.0', port: 9000 }, // net.ListenOptions
1886
- authenticateOptions: {
1887
- required: true, // Require client auth
1888
- duration: 5000, // 5s to authenticate
1889
- },
1890
- extraEvents: {
1891
- 'timeout': ({ id, socket }) => {
1892
- console.log(`Client ${id} timed out`);
1893
- },
1894
- },
1895
- onServerReady: ({ server }) => {
1896
- console.log('Server listening on', server.address());
1897
- },
1898
- onClientConnected: ({ id, socket }) => {
1899
- console.log(`Client ${id} connected from ${socket.remoteAddress}:${socket.remotePort}`);
1900
- // Client starts as 'unauthorized' when authenticateOptions.required = true
1901
- },
1902
- onClientData: ({ id, data }) => {
1903
- console.log(`[${id}] Data:`, data.toString());
1904
- },
1905
- onClientClose: ({ id }) => {
1906
- console.log(`Client ${id} disconnected`);
1907
- },
1908
- onClientError: ({ id, error }) => {
1909
- console.error(`Client ${id} error:`, error);
1910
- },
1911
- });
1912
-
1913
- // Authentication management
1914
- server.doAuthenticate({ id: clientId, state: 'authenticated' });
1915
- // States: 'unauthorized' | 'authenticating' | 'authenticated'
1916
-
1917
- // Send data to a client
1918
- server.emit({ clientId: id, payload: 'Hello client!' });
1919
- server.emit({ clientId: id, payload: Buffer.from([0x01, 0x02]) });
1920
-
1921
- // Get connected clients
1922
- const clients = server.getClients(); // Record<string, ITcpSocketClient>
1923
- const client = server.getClient({ id: clientId });
1924
- // { id, socket, state, subscriptions: Set, storage: { connectedAt, authenticatedAt, ... } }
1925
- ```
1926
-
1927
- **Client:**
1928
-
1929
- ```typescript
1930
- const client = new NetworkTcpClient({
1931
- identifier: 'data-client',
1932
- options: { host: '127.0.0.1', port: 9000 }, // net.TcpSocketConnectOpts
1933
- reconnect: true, // Auto-reconnect on error (default: false)
1934
- maxRetry: 5, // Max reconnection attempts (default: 5)
1935
- encoding: 'utf-8', // Socket encoding (optional)
1936
- onConnected: ({ client }) => {
1937
- console.log('Connected to server');
1938
- },
1939
- onData: ({ identifier, message }) => {
1940
- console.log(`[${identifier}] Received:`, message);
1941
- },
1942
- onClosed: ({ client }) => {
1943
- console.log('Connection closed');
1944
- },
1945
- onError: (error) => {
1946
- console.error('Error:', error);
1947
- // Auto-reconnect fires after 5000ms if reconnect: true
1948
- },
1949
- });
1950
-
1951
- client.connect({ resetReconnectCounter: true });
1952
- client.emit({ payload: 'Hello server!' });
1953
- client.emit({ payload: Buffer.from([0x01, 0x02]) });
1954
-
1955
- client.isConnected(); // boolean
1956
- client.forceReconnect(); // Destroy + reconnect with counter reset
1957
- client.disconnect(); // Destroy + clear reconnect timer
1958
- ```
1959
-
1960
- **TLS variants:** `NetworkTLSTcpServer` and `NetworkTLSTcpClient` accept `tls.TLSSocketOptions` and `tls.ConnectionOptions` respectively. They use the same API but with encrypted connections.
1961
-
1962
- ### UDP
1963
-
1964
- ```typescript
1965
- import { NetworkUdpClient } from '@venizia/ignis-helpers';
1966
-
1967
- // Unicast
1968
- const udpSimple = NetworkUdpClient.newInstance({
1969
- identifier: 'udp-sender',
1970
- port: 5000,
1971
- host: '0.0.0.0',
1972
- onData: ({ message, remoteInfo }) => {
1973
- console.log(`From ${remoteInfo.address}:${remoteInfo.port}:`, message.toString());
1974
- },
1975
- });
1976
- udpSimple.connect();
1977
-
1978
- // Multicast
1979
- const udp = NetworkUdpClient.newInstance({
1980
- identifier: 'sensor-listener',
1981
- port: 5000,
1982
- reuseAddr: true,
1983
- multicastAddress: {
1984
- groups: ['239.1.2.3', '239.1.2.4'],
1985
- interface: '0.0.0.0',
1986
- },
1987
- onData: ({ message, remoteInfo }) => {
1988
- console.log(`From ${remoteInfo.address}:${remoteInfo.port}:`, message.toString());
1989
- },
1990
- onBind: async ({ socket, multicastAddress }) => {
1991
- // Join multicast groups after binding
1992
- for (const group of multicastAddress?.groups ?? []) {
1993
- socket.addMembership(group, multicastAddress?.interface);
1994
- }
1995
- },
1996
- onConnected: ({ identifier, host, port }) => {
1997
- console.log(`${identifier} listening on ${host}:${port}`);
1998
- },
1999
- onClosed: ({ identifier }) => {
2000
- console.log(`${identifier} closed`);
2001
- },
2002
- onError: ({ identifier, error }) => {
2003
- console.error(`${identifier} error:`, error);
2004
- },
2005
- });
2006
-
2007
- udp.connect();
2008
- udp.isConnected(); // boolean
2009
- udp.disconnect();
2010
- ```
2011
-
2012
- ---
2013
-
2014
- ## UID (Snowflake)
2015
-
2016
- Snowflake ID generator producing unique, time-sortable IDs with Base62 encoding.
2017
-
2018
- ### Bit Layout
2019
-
2020
- ```
2021
- 70-bit Snowflake ID
2022
- +-----------------+----------+----------+
2023
- | Timestamp | Worker | Sequence |
2024
- | (48 bits) | (10 bits)| (12 bits)|
2025
- +-----------------+----------+----------+
2026
- MSB LSB
2027
- ```
2028
-
2029
- | Field | Bits | Range | Description |
2030
- | --------- | ---- | ----------------------------- | ------------------------------ |
2031
- | Timestamp | 48 | ~8,919 years from epoch | Milliseconds since epoch |
2032
- | Worker ID | 10 | 0--1023 (1024 workers) | Unique worker identifier |
2033
- | Sequence | 12 | 0--4095 per millisecond | Per-millisecond counter |
2034
-
2035
- **Output:** Base62 encoded string (10--12 characters).
2036
- **Default epoch:** 2025-01-01 00:00:00 UTC (`1735689600000`).
2037
- **Lifespan:** Until approximately 10,944 AD.
2038
-
2039
- ### UID API Reference
2040
-
2041
- ```typescript
2042
- import { SnowflakeUidHelper, SnowflakeConfig } from '@venizia/ignis-helpers';
2043
-
2044
- // Default: workerId=199, epoch=2025-01-01
2045
- const uid = new SnowflakeUidHelper();
2046
-
2047
- // Custom configuration
2048
- const uid = new SnowflakeUidHelper({
2049
- workerId: 42, // 0-1023
2050
- epoch: BigInt(1735689600000), // Custom epoch
2051
- });
2052
-
2053
- // --- Generate IDs ---
2054
-
2055
- const id = uid.nextId(); // Base62 string, e.g., "9du1sJXO88"
2056
- const snowflake = uid.nextSnowflake(); // Raw BigInt, e.g., 130546360012247045n
2057
-
2058
- // --- Parse an ID back to components ---
2059
-
2060
- const parsed = uid.parseId('9du1sJXO88');
2061
- // {
2062
- // raw: 130546360012247045n,
2063
- // timestamp: Date, // When the ID was generated
2064
- // workerId: 199, // Which worker generated it
2065
- // sequence: 0, // Sequence within that millisecond
2066
- // }
2067
-
2068
- // --- Extract individual components from raw snowflake ---
2069
-
2070
- const ts = uid.extractTimestamp(snowflake); // Date
2071
- const wid = uid.extractWorkerId(snowflake); // number (0-1023)
2072
- const seq = uid.extractSequence(snowflake); // number (0-4095)
2073
-
2074
- // --- Base62 encoding/decoding ---
2075
-
2076
- const encoded = uid.encodeBase62(BigInt(123456789)); // "8m0Kx"
2077
- const decoded = uid.decodeBase62('8m0Kx'); // 123456789n
2078
-
2079
- // --- Configuration constants ---
2080
-
2081
- SnowflakeConfig.DEFAULT_EPOCH; // 1735689600000n (2025-01-01)
2082
- SnowflakeConfig.MAX_WORKER_ID; // 1023n
2083
- SnowflakeConfig.MAX_SEQUENCE; // 4095n
2084
- SnowflakeConfig.MAX_CLOCK_BACKWARD_MS; // 100n (tolerance for clock drift)
2085
-
2086
- // Get current worker ID
2087
- uid.getWorkerId(); // number
2088
- ```
2089
-
2090
- ### Throughput and Limits
2091
-
2092
- | Metric | Value |
2093
- | ---------------------------------------- | -------------------------------- |
2094
- | **IDs per millisecond per worker** | 4,096 |
2095
- | **IDs per second per worker** | 4,096,000 |
2096
- | **Max workers** | 1,024 |
2097
- | **Total IDs per second (all workers)** | ~4.2 billion |
2098
- | **ID lifespan** | ~8,919 years from epoch |
2099
- | **Clock backward tolerance** | 100ms (waits), >100ms throws |
2100
- | **Base62 output length** | 10--12 characters |
2101
- | **Base62 charset** | `0-9A-Za-z` |
2102
-
2103
- **Clock drift handling:**
2104
-
2105
- - If clock moves backward <= 100ms: waits (spin-loop) until clock catches up
2106
- - If clock moves backward > 100ms: throws `ApplicationError` (500)
2107
- - If sequence exhausted in a millisecond (>4095): waits for next millisecond
2108
-
2109
- **Expiry warning:** The generator logs a warning when approaching the 48-bit timestamp limit (~10 years before expiry). Action: plan migration to a new epoch.
2110
-
2111
- ---
2112
-
2113
- ## Environment
2114
-
2115
- ### Environment Detection
2116
-
2117
- ```typescript
2118
- import { Environment } from '@venizia/ignis-helpers';
2119
-
2120
- const env = Environment.current; // process.env.NODE_ENV or 'development'
2121
- const isProd = Environment.is({ name: 'production' });
2122
-
2123
- // Available constants
2124
- Environment.LOCAL; // 'local'
2125
- Environment.DEBUG; // 'debug'
2126
- Environment.DEVELOPMENT; // 'development'
2127
- Environment.ALPHA; // 'alpha'
2128
- Environment.BETA; // 'beta'
2129
- Environment.STAGING; // 'staging'
2130
- Environment.PRODUCTION; // 'production'
2131
-
2132
- // Common environment set (used for debug level gating)
2133
- Environment.COMMON_ENVS; // Set containing all above values
2134
- ```
2135
-
2136
- ### Application Environment
2137
-
2138
- Prefixed environment variable management.
2139
-
2140
- ```typescript
2141
- import { applicationEnvironment, ApplicationEnvironment } from '@venizia/ignis-helpers';
2142
-
2143
- // Default instance uses prefix 'APP_ENV' (configurable via APPLICATION_ENV_PREFIX env var)
2144
- const dbHost = applicationEnvironment.get<string>('APP_ENV_DB_HOST');
2145
- applicationEnvironment.set('APP_ENV_FEATURE_FLAG', true);
2146
-
2147
- const isDev = applicationEnvironment.isDevelopment();
2148
- const keys = applicationEnvironment.keys();
2149
- // Only returns keys starting with the configured prefix
2150
-
2151
- // Custom prefix
2152
- const myEnv = new ApplicationEnvironment({
2153
- prefix: 'MY_APP',
2154
- envs: process.env,
2155
- });
2156
- const val = myEnv.get<string>('MY_APP_SECRET');
2157
- ```
2158
-
2159
- ### Runtime Detection
2160
-
2161
- ```typescript
2162
- import { RuntimeModules } from '@venizia/ignis-helpers';
2163
-
2164
- const runtime = RuntimeModules.detect(); // 'bun' or 'node'
2165
- const isBun = RuntimeModules.isBun(); // Checks: typeof Bun !== 'undefined'
2166
- const isNode = RuntimeModules.isNode();
2167
- ```
2168
-
2169
- ---
2170
-
2171
- ## Error
2172
-
2173
- Standardized error class with HTTP status codes and optional message codes.
2174
-
2175
- ```typescript
2176
- import { ApplicationError, getError } from '@venizia/ignis-helpers';
2177
-
2178
- // Using factory function (preferred)
2179
- throw getError({
2180
- statusCode: 404,
2181
- message: 'User not found',
2182
- messageCode: 'user.not_found', // Optional machine-readable code
2183
- });
2184
-
2185
- // Using class constructor
2186
- throw new ApplicationError({
2187
- statusCode: 400,
2188
- message: 'Invalid input',
2189
- });
2190
-
2191
- // Static factory
2192
- throw ApplicationError.getError({
2193
- statusCode: 500,
2194
- message: 'Internal error',
2195
- });
2196
-
2197
- // ApplicationError extends Error
2198
- try {
2199
- throw getError({ statusCode: 403, message: 'Forbidden', messageCode: 'auth.forbidden' });
2200
- } catch (error) {
2201
- if (error instanceof ApplicationError) {
2202
- console.log(error.statusCode); // 403
2203
- console.log(error.message); // 'Forbidden'
2204
- console.log(error.messageCode); // 'auth.forbidden'
2205
- console.log(error.stack); // Standard Error stack trace
2206
- }
2207
- }
2208
- ```
2209
-
2210
- **Error Zod schema (for OpenAPI):**
2211
-
2212
- ```typescript
2213
- import { ErrorSchema } from '@venizia/ignis-helpers';
2214
-
2215
- // ErrorSchema is a Zod schema used in OpenAPI route definitions
2216
- // { name?: string, statusCode?: number, messageCode?: string, message: string }
2217
- ```
2218
-
2219
- ---
2220
-
2221
- ## Worker Thread
2222
-
2223
- Worker pool management and inter-thread messaging.
2224
-
2225
- ### Worker Pool
2226
-
2227
- Singleton pool that tracks worker threads up to the number of CPU cores.
2228
-
2229
- ```typescript
2230
- import { WorkerPoolHelper, BaseWorkerHelper } from '@venizia/ignis-helpers';
2231
-
2232
- const pool = WorkerPoolHelper.getInstance(); // Singleton
2233
-
2234
- // Create and register a worker
2235
- const worker = new BaseWorkerHelper({
2236
- identifier: 'image-processor',
2237
- path: './workers/image-processor.ts',
2238
- options: { workerData: { quality: 80 } }, // WorkerOptions
2239
- eventHandlers: {
2240
- onOnline: () => console.log('Worker online'),
2241
- onMessage: ({ message }) => console.log('Result:', message),
2242
- onError: ({ error }) => console.error('Worker error:', error),
2243
- onExit: ({ code }) => console.log('Worker exited:', code),
2244
- onMessageError: ({ error }) => console.error('Message error:', error),
2245
- },
2246
- });
2247
-
2248
- pool.register({ key: 'image-processor', worker });
2249
-
2250
- // Check pool
2251
- pool.has({ key: 'image-processor' }); // true
2252
- pool.size(); // 1
2253
- pool.get({ key: 'image-processor' }); // BaseWorkerHelper instance
2254
-
2255
- // Unregister (terminates worker thread)
2256
- await pool.unregister({ key: 'image-processor' });
2257
-
2258
- // Pool warns when size reaches CPU core count (configurable via ignoreMaxWarning)
2259
- ```
2260
-
2261
- ### Worker Bus
2262
-
2263
- Message bus for structured inter-thread communication using `MessagePort`.
2264
-
2265
- ```typescript
2266
- import {
2267
- BaseWorkerBusHelper,
2268
- BaseWorkerMessageBusHandlerHelper,
2269
- BaseWorkerThreadHelper,
2270
- } from '@venizia/ignis-helpers';
2271
-
2272
- // --- Inside worker thread ---
2273
- const thread = new BaseWorkerThreadHelper({ scope: 'ImageWorker' });
2274
- // Throws if called from main thread
2275
-
2276
- const busHandler = new BaseWorkerMessageBusHandlerHelper({
2277
- scope: 'image-bus-handler',
2278
- onMessage: ({ message }) => {
2279
- console.log('Received from main:', message);
2280
- },
2281
- onClose: () => console.log('Bus closed'),
2282
- onError: ({ error }) => console.error('Bus error:', error),
2283
- onExit: ({ exitCode }) => console.log('Bus exited:', exitCode),
2284
- });
2285
-
2286
- const bus = new BaseWorkerBusHelper({
2287
- scope: 'image-bus',
2288
- port: parentPort!, // MessagePort from worker_threads
2289
- busHandler,
2290
- });
2291
-
2292
- thread.bindWorkerBus({ key: 'main', bus });
2293
-
2294
- // Send message to main thread
2295
- bus.postMessage({
2296
- message: { type: 'result', data: processedImage },
2297
- transferList: undefined, // Optional Transferable[]
2298
- });
2299
-
2300
- // Lifecycle hooks
2301
- // bus.onBeforePostMessage = ({ message }) => { ... };
2302
- // bus.onAfterPostMessage = ({ message }) => { ... };
2303
-
2304
- // Retrieve a bound bus
2305
- const mainBus = thread.getWorkerBus({ key: 'main' });
2306
-
2307
- // Unbind (removes all listeners)
2308
- thread.unbindWorkerBus({ key: 'main' });
2309
- ```
2310
-
2311
- ---
2312
-
2313
- ## Common Utilities
2314
-
2315
- ### Date Utility
2316
-
2317
- Day.js with UTC, timezone, weekday, isoWeek, and custom parse format plugins pre-configured.
2318
-
2319
- ```typescript
2320
- import {
2321
- dayjs,
2322
- sleep,
2323
- isWeekday,
2324
- getPreviousWeekday,
2325
- getNextWeekday,
2326
- getDateTz,
2327
- hrTime,
2328
- } from '@venizia/ignis-helpers';
2329
-
2330
- // Default timezone: Asia/Ho_Chi_Minh (configurable via APP_ENV_APPLICATION_TIMEZONE)
2331
- const now = dayjs();
2332
- const utc = dayjs.utc();
2333
-
2334
- // Sleep utility
2335
- await sleep(1000); // Sleep 1 second
2336
-
2337
- // Weekday checks (Monday=1 through Friday=5)
2338
- isWeekday('2025-01-06'); // true (Monday)
2339
- isWeekday('2025-01-05'); // false (Sunday)
2340
-
2341
- // Navigate to adjacent weekdays
2342
- const prevWeekday = getPreviousWeekday(); // Previous weekday from today
2343
- const nextWeekday = getNextWeekday(); // Next weekday from today
2344
- const prevFromDate = getPreviousWeekday({ date: '2025-01-06' });
2345
-
2346
- // Timezone conversion
2347
- const tzDate = getDateTz({
2348
- date: '2025-01-01T00:00:00Z',
2349
- timezone: 'America/New_York',
2350
- useClientTz: false, // default
2351
- timeOffset: -5, // Additional hour offset
2352
- });
2353
-
2354
- // High-resolution time (seconds with 9 decimal places)
2355
- const elapsed = hrTime(); // e.g., 1234567.890123456
2356
- ```
2357
-
2358
- ### Parse Utility
2359
-
2360
- ```typescript
2361
- import {
2362
- int, float, isInt, isFloat, toBoolean,
2363
- toCamel, keysToCamel,
2364
- toStringDecimal, getNumberValue, getUID,
2365
- parseArrayToRecordWithKey, parseArrayToMapWithKey,
2366
- } from '@venizia/ignis-helpers';
2367
-
2368
- // --- Numeric parsing ---
2369
- int('42'); // 42
2370
- int('3.14'); // 3
2371
- int('1,234'); // 1234 (removes commas)
2372
- int(null); // 0
2373
- int(NaN); // 0
2374
-
2375
- float('3.14159', 2); // 3.14
2376
- float('3.14159', 4); // 3.1416 (rounds using lodash.round)
2377
- float('1,234.56'); // 1234.56
2378
-
2379
- isInt(42); // true
2380
- isInt(3.14); // false
2381
- isFloat(3.14); // true
2382
- isFloat(42); // true (integers are also valid floats)
2383
-
2384
- // --- Boolean parsing ---
2385
- toBoolean('true'); // true
2386
- toBoolean(''); // false
2387
- toBoolean('false'); // false
2388
- toBoolean('0'); // false
2389
- toBoolean(0); // false
2390
- toBoolean(null); // false
2391
- toBoolean(undefined); // false
2392
- toBoolean(1); // true
2393
- toBoolean('yes'); // true (any non-falsy string)
2394
-
2395
- // --- Case conversion ---
2396
- toCamel('snake_case'); // 'snakeCase'
2397
- toCamel('kebab-case'); // 'kebabCase'
2398
- toCamel('already_camelCase'); // 'alreadyCamelCase'
2399
-
2400
- keysToCamel({ user_name: 'Alice', home_address: { zip_code: '12345' } });
2401
- // { userName: 'Alice', homeAddress: { zipCode: '12345' } }
2402
- // Recursively converts nested objects (but not arrays or Dates)
2403
-
2404
- // --- Number formatting ---
2405
- toStringDecimal(1234567.891, 2); // '1,234,567.89' (en-US locale)
2406
- toStringDecimal(1234567, 0); // '1,234,567'
2407
- toStringDecimal(3.14159, 4); // '3.1416'
2408
- toStringDecimal(42, 2, { useLocaleFormat: false }); // '42.00'
2409
-
2410
- // EU number format support
2411
- getNumberValue('1.234,56', { locale: 'eu' }); // 1234 (parsed as int)
2412
- getNumberValue('1.234,56', { locale: 'eu', method: 'float' }); // 1234.56
2413
- getNumberValue('1,234.56', { locale: 'us' }); // 1234 (default locale)
2414
-
2415
- // --- Random ID ---
2416
- getUID(); // Random string, e.g., 'K7X2F9ZA1B' (Math.random base36)
2417
-
2418
- // --- Array to keyed collections ---
2419
- const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
2420
-
2421
- const record = parseArrayToRecordWithKey({ arr: users, keyMap: 'id' });
2422
- // { 1: { id: 1, name: 'A' }, 2: { id: 2, name: 'B' } }
2423
-
2424
- const map = parseArrayToMapWithKey(users, 'id');
2425
- // Map { 1 => { id: 1, name: 'A' }, 2 => { id: 2, name: 'B' } }
2426
- // Note: Duplicate keys = last element wins in both cases
2427
- ```
2428
-
2429
- ### Promise Utility
2430
-
2431
- ```typescript
2432
- import {
2433
- executePromiseWithLimit,
2434
- isPromiseLike,
2435
- getDeepProperty,
2436
- transformValueOrPromise,
2437
- } from '@venizia/ignis-helpers';
2438
-
2439
- // --- Parallel task execution with concurrency limit ---
2440
- // Execute 100 tasks with max 10 concurrent
2441
- const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/item/${i}`);
2442
- const tasks = urls.map(url => () => fetch(url).then(r => r.json()));
2443
-
2444
- const results = await executePromiseWithLimit({
2445
- tasks, // Array<() => Promise<T>>
2446
- limit: 10, // Max concurrent promises
2447
- onTaskDone: ({ result }) => {
2448
- console.log('Task completed:', result);
2449
- },
2450
- });
2451
- // results: all 100 resolved values in order
2452
-
2453
- // --- Promise detection ---
2454
- isPromiseLike(Promise.resolve(1)); // true
2455
- isPromiseLike({ then: () => {} }); // true (thenable)
2456
- isPromiseLike(42); // false
2457
- isPromiseLike(null); // false
2458
-
2459
- // --- Deep property access ---
2460
- const obj = { a: { b: { c: 42 } } };
2461
- const value = getDeepProperty(obj, 'a.b.c'); // 42
2462
- // Throws ApplicationError if any part of the path is null/undefined
2463
-
2464
- // --- Transform value or promise ---
2465
- const transformed = await transformValueOrPromise(
2466
- fetchData(), // ValueOrPromise<T>
2467
- (data) => data.map(item => item.name), // Transformer function
2468
- );
2469
- // Works with both sync values and promises
2470
- ```
2471
-
2472
- ### Request Utility
2473
-
2474
- ```typescript
2475
- import {
2476
- parseMultipartBody,
2477
- sanitizeFilename,
2478
- encodeRFC5987,
2479
- createContentDispositionHeader,
2480
- } from '@venizia/ignis-helpers';
2481
-
2482
- // --- Parse multipart form data (Hono context) ---
2483
-
2484
- // Memory storage (default) -- files kept as Buffer
2485
- const files = await parseMultipartBody({
2486
- context: c, // Hono Context (or anything with .req.formData())
2487
- storage: 'memory', // 'memory' | 'disk'
2488
- });
2489
- // Returns: IParsedFile[]
2490
- // [{ fieldname, originalname, encoding, mimetype, size, buffer }]
2491
-
2492
- // Disk storage -- files written to filesystem
2493
- const diskFiles = await parseMultipartBody({
2494
- context: c,
2495
- storage: 'disk',
2496
- uploadDir: './uploads', // Default: './uploads' (created if missing)
2497
- });
2498
- // Returns: IParsedFile[]
2499
- // [{ fieldname, originalname, encoding, mimetype, size, filename, path }]
2500
- // filename format: '<timestamp>-<random>-<sanitized-original>'
2501
-
2502
- // --- Filename sanitization ---
2503
- sanitizeFilename('photo.jpg'); // 'photo.jpg'
2504
- sanitizeFilename('../../../etc/passwd'); // '______etc_passwd' or 'download'
2505
- sanitizeFilename('.hidden'); // 'hidden' (leading dot removed)
2506
- sanitizeFilename('file with spaces.txt'); // 'file with spaces.txt'
2507
- sanitizeFilename(''); // 'download' (fallback)
2508
- sanitizeFilename('...'); // 'download' (fallback)
2509
- // Allowed characters: alphanumeric, spaces, hyphens, underscores, dots
2510
-
2511
- // --- RFC 5987 encoding ---
2512
- encodeRFC5987("report (2025).pdf");
2513
- // 'report%20%282025%29.pdf'
2514
-
2515
- // --- Content-Disposition header (RFC 5987 compliant) ---
2516
- const header = createContentDispositionHeader({
2517
- filename: 'report (2025).pdf',
2518
- type: 'attachment', // 'attachment' | 'inline'
2519
- });
2520
- // 'attachment; filename="report__2025_.pdf"; filename*=UTF-8\'\'report__2025_.pdf'
2521
- // Includes both ASCII fallback (filename=) and UTF-8 encoded (filename*=) for compatibility
2522
- ```
2523
-
2524
- ### Performance Utility
2525
-
2526
- ```typescript
2527
- import {
2528
- getPerformanceCheckpoint,
2529
- getExecutedPerformance,
2530
- executeWithPerformanceMeasure,
2531
- } from '@venizia/ignis-helpers';
2532
-
2533
- // --- Manual checkpoints ---
2534
- const start = getPerformanceCheckpoint(); // performance.now()
2535
-
2536
- // ... do work ...
2537
-
2538
- const elapsed = getExecutedPerformance({ from: start, digit: 3 });
2539
- // e.g., 142.567 (ms, 3 decimal places)
2540
- // Default digit: 6
2541
-
2542
- // --- Automatic measurement with logging ---
2543
- const result = await executeWithPerformanceMeasure({
2544
- scope: 'DataSync',
2545
- description: 'Syncing user data',
2546
- logger, // Logger instance (falls back to console)
2547
- level: 'debug', // Log level (default: 'debug')
2548
- args: { userId: 123 }, // Optional context args (logged)
2549
- task: () => syncUserData(),
2550
- });
2551
- // Logs:
2552
- // [DataSync] START | Syncing user data... | Args: {"userId":123}
2553
- // [DataSync] DONE | Syncing user data | Args: {"userId":123} | Took: 142.567 (ms)
2554
- // Returns: result of syncUserData()
2555
- ```
2556
-
2557
- ### Module Utility
2558
-
2559
- Pre-flight check for optional dependencies before using them. Throws a clear error with install instructions if any module is missing.
2560
-
2561
- ```typescript
2562
- import { validateModule } from '@venizia/ignis-helpers';
2563
-
2564
- // Check single module
2565
- await validateModule({
2566
- scope: 'SocketIO',
2567
- modules: ['socket.io'],
2568
- });
2569
-
2570
- // Check multiple modules
2571
- await validateModule({
2572
- scope: 'SocketIO',
2573
- modules: ['socket.io', '@socket.io/redis-adapter'],
2574
- });
2575
- // If socket.io is missing:
2576
- // throws ApplicationError:
2577
- // "[validateModule] socket.io is required for SocketIO. Please install 'socket.io'"
2578
-
2579
- // Without scope
2580
- await validateModule({
2581
- modules: ['bullmq'],
2582
- });
2583
- // If bullmq is missing:
2584
- // throws: "[validateModule] bullmq is required. Please install 'bullmq'"
2585
- ```
2586
-
2587
- This utility is used internally by all sub-path modules (BullMQ, MQTT, MinIO, Socket.IO, Axios, Cron, Kafka) to provide clear error messages when peer dependencies are not installed.
2588
-
2589
- ---
2590
-
2591
- ## Common Types
2592
-
2593
- ```typescript
2594
- // Nullable
2595
- type TNullable<T> = T | undefined | null;
2596
-
2597
- // Flexible types
2598
- type AnyType = any;
2599
- type AnyObject = Record<string | symbol | number, any>;
2600
-
2601
- // Value types
2602
- type ValueOrPromise<T> = T | Promise<T>;
2603
- type ValueOf<T> = T[keyof T];
2604
- type ValueOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
2605
- type ValueOptionalExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
2606
-
2607
- // Class types
2608
- type TConstructor<T> = new (...args: any[]) => T;
2609
- type TAbstractConstructor<T> = abstract new (...args: any[]) => T;
2610
- type TClass<T> = TConstructor<T> & { [property: string]: any };
2611
- type TAbstractClass<T> = TAbstractConstructor<T> & { [property: string]: any };
2612
-
2613
- // Mixin types
2614
- type TMixinTarget<T> = TConstructor<{ [P in keyof T]: T[P] }>;
2615
- type TAbstractMixinTarget<T> = TAbstractConstructor<{ [P in keyof T]: T[P] }>;
2616
-
2617
- // Resolver types
2618
- type TResolver<T> = (...args: any[]) => T;
2619
- type TAsyncResolver<T> = (...args: any[]) => T | Promise<T>;
2620
- type TValueOrResolver<T> = T | TResolver<T>;
2621
-
2622
- // Resolver helpers
2623
- resolveValue<T>(valueOrResolver) // Resolve sync value or resolver (handles class constructors)
2624
- resolveValueAsync<T>(valueOrResolver) // Resolve async value or resolver
2625
- resolveClass<T>(ref) // Resolve class reference or string binding key
2626
-
2627
- // Configuration
2628
- interface IConfigurable<Options, Result> {
2629
- configure(opts?: Options): ValueOrPromise<Result>;
2630
- }
2631
-
2632
- // Constants
2633
- RuntimeModules.detect() / .isBun() / .isNode()
2634
- DataTypes.NUMBER | TEXT | BYTE | JSON | BOOLEAN
2635
- HTTP.Headers, HTTP.Methods, HTTP.ResultCodes, HTTP.HeaderValues
2636
- MimeTypes.IMAGE | VIDEO | TEXT | UNKNOWN
2637
-
2638
- // Field Mapping Types (for dynamic schemas)
2639
- type TFieldMappingDataType = 'string' | 'number' | 'strings' | 'numbers' | 'boolean';
2640
- interface IFieldMapping { name: string; type: TFieldMappingDataType; default?: any; }
2641
-
2642
- // JSX (re-exported from Hono)
2643
- type { Child, FC, PropsWithChildren } from 'hono/jsx';
2644
- ```
2645
-
2646
- ---
2647
-
2648
- ## Key Patterns
2649
-
2650
- ### BaseHelper
2651
-
2652
- All helpers extend `BaseHelper` which provides scoped logging automatically:
2653
-
2654
- ```typescript
2655
- class BaseHelper {
2656
- scope: string;
2657
- identifier: string;
2658
- logger: Logger;
2659
-
2660
- constructor(opts: { scope: string; identifier?: string });
2661
-
2662
- getIdentifier(): string;
2663
- getLogger(): Logger;
2664
- }
2665
- ```
2666
-
2667
- Usage in helpers:
2668
-
2669
- ```typescript
2670
- class MyHelper extends BaseHelper {
2671
- constructor() {
2672
- super({ scope: 'MyHelper', identifier: 'instance-1' });
2673
- }
2674
-
2675
- doWork() {
2676
- this.logger.for('doWork').info('Starting...');
2677
- this.logger.for('doWork').debug('Detail: %j', data);
2678
- }
2679
- }
2680
- ```
2681
-
2682
- The logger scope is constructed as `[scope-identifier]` (or just `[scope]` if no identifier). Method-scoped loggers append `-methodName`.
2683
-
2684
- ### Options Objects
2685
-
2686
- All APIs use options objects instead of positional parameters:
2687
-
2688
- ```typescript
2689
- // Correct
2690
- redis.get({ key: 'user:1' });
2691
- aes.encrypt({ message: 'data', secret: 'key' });
2692
- pool.register({ key: 'worker-1', worker: myWorker });
2693
-
2694
- // NOT positional arguments
2695
- // redis.get('user:1'); -- wrong
2696
- // aes.encrypt('data', 'key') -- wrong
2697
- ```
2698
-
2699
- ### Event Lifecycle
2700
-
2701
- Helpers that manage connections follow a consistent lifecycle hook pattern:
2702
-
2703
- ```typescript
2704
- {
2705
- onInitialized?: (opts) => void; // Instance created
2706
- onConnected?: (opts) => void; // Connection established
2707
- onReady?: (opts) => void; // Ready for operations
2708
- onError?: (opts) => void; // Error occurred
2709
- }
2710
- ```
2711
-
2712
- ### IConfigurable
2713
-
2714
- Helpers that require async initialization implement `IConfigurable`:
2715
-
2716
- ```typescript
2717
- interface IConfigurable<Options, Result> {
2718
- configure(opts?: Options): ValueOrPromise<Result>;
2719
- }
2720
- ```
2721
-
2722
- ---
2723
-
2724
- ## Integration with IGNIS Core
2725
-
2726
- The helpers package is designed to integrate with the IGNIS framework's IoC container. Here is how helpers are typically used within an IGNIS application:
2727
-
2728
- **Binding helpers in the application lifecycle:**
2729
-
2730
- ```typescript
2731
- import { BaseApplication } from '@venizia/ignis';
2732
- import { BootMixin } from '@venizia/ignis-boot';
2733
- import { RedisSingleHelper } from '@venizia/ignis-helpers';
2734
-
2735
- class MyApplication extends BootMixin(BaseApplication) {
2736
- async preConfigure() {
2737
- // BaseApplication extends the IoC Container directly -- bind on `this`, not `this.container`.
2738
- this.bind({ key: 'datasources.Redis' }).toValue(
2739
- new RedisSingleHelper({
2740
- name: 'main',
2741
- host: process.env.REDIS_HOST,
2742
- port: parseInt(process.env.REDIS_PORT),
2743
- password: process.env.REDIS_PASSWORD,
2744
- })
2745
- );
2746
-
2747
- // Register other helpers...
2748
- await super.preConfigure();
2749
- }
2750
- }
2751
- ```
2752
-
2753
- **Injecting helpers in services:**
2754
-
2755
- ```typescript
2756
- import { inject } from '@venizia/ignis-inversion';
2757
- import { BaseService } from '@venizia/ignis';
2758
- import { IRedisHelper } from '@venizia/ignis-helpers';
2759
-
2760
- export class CacheService extends BaseService {
2761
- constructor(
2762
- @inject({ key: 'datasources.Redis' }) private redis: IRedisHelper,
2763
- ) {
2764
- super({ scope: CacheService.name });
2765
- }
2766
-
2767
- async getCached(key: string) {
2768
- return this.redis.getObject({ key });
2769
- }
2770
- }
2771
- ```
2772
-
2773
- **Using helpers in controllers:**
2774
-
2775
- ```typescript
2776
- import { BaseRestController, controller, get, jsonResponse, TRouteContext } from '@venizia/ignis';
2777
- import { inject } from '@venizia/ignis-inversion';
2778
- import { IRedisHelper } from '@venizia/ignis-helpers';
2779
- import { z } from '@hono/zod-openapi';
2780
-
2781
- @controller({ path: '/health' })
2782
- export class HealthController extends BaseRestController {
2783
- constructor(
2784
- @inject({ key: 'datasources.Redis' }) private redis: IRedisHelper,
2785
- ) {
2786
- super({ scope: HealthController.name });
2787
- }
2788
-
2789
- override binding() {}
2790
-
2791
- @get({
2792
- configs: {
2793
- path: '/',
2794
- responses: jsonResponse({ schema: z.object({ redis: z.string() }) }),
2795
- },
2796
- })
2797
- async check(context: TRouteContext) {
2798
- const ping = await this.redis.ping();
2799
- return context.json({ redis: ping === 'PONG' ? 'up' : 'down' }, 200);
2800
- }
2801
- }
2802
- ```
2803
-
2804
- ---
2805
-
2806
- ## Environment Variables Reference
2807
-
2808
- Complete table of all environment variables used across all helpers modules.
2809
-
2810
- ### Logger
2811
-
2812
- | Variable | Default | Module | Description |
2813
- | ----------------------------------- | -------------- | ------- | ---------------------------------------- |
2814
- | `APP_ENV_LOGGER_FOLDER_PATH` | `./` | Logger | Directory for log files |
2815
- | `APP_ENV_LOGGER_FORMAT` | `text` | Logger | Log format: `json` or `text` |
2816
- | `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | Logger | File rotation frequency |
2817
- | `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Logger | Max file size before rotation |
2818
- | `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Logger | Max retention period |
2819
- | `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Logger | Date pattern for filenames |
2820
- | `APP_ENV_LOGGER_DGRAM_HOST` | -- | Logger | UDP transport host |
2821
- | `APP_ENV_LOGGER_DGRAM_PORT` | -- | Logger | UDP transport port |
2822
- | `APP_ENV_LOGGER_DGRAM_LABEL` | -- | Logger | UDP transport label |
2823
- | `APP_ENV_LOGGER_DGRAM_LEVELS` | -- | Logger | Comma-separated UDP levels |
2824
- | `DEBUG` | -- | Logger | Enable debug level output |
2825
- | `APP_ENV_EXTRA_LOG_ENVS` | -- | Logger | Extra NODE_ENV values for debug |
2826
-
2827
- ### Application
2828
-
2829
- | Variable | Default | Module | Description |
2830
- | ---------------------------- | ------------------ | --------------- | ------------------------------------- |
2831
- | `APP_ENV_APPLICATION_NAME` | `APP` | Logger/Defaults | Application name prefix |
2832
- | `APP_ENV_APPLICATION_TIMEZONE` | `Asia/Ho_Chi_Minh` | Date utility | Default timezone for dayjs |
2833
- | `APPLICATION_ENV_PREFIX` | `APP_ENV` | Environment | Prefix for ApplicationEnvironment |
2834
- | `NODE_ENV` | `development` | Environment | Runtime environment name |
2835
-
2836
- ---
2837
-
2838
- ## Error Handling Patterns
2839
-
2840
- ### Connection-based Helpers (Redis, MQTT, TCP, etc.)
2841
-
2842
- All connection helpers use lifecycle hooks for error handling. Errors are never swallowed -- they are logged and forwarded to your callback:
2843
-
2844
- ```typescript
2845
- const redis = new RedisSingleHelper({
2846
- // ...
2847
- onError: ({ name, helper, error }) => {
2848
- // Log to external monitoring
2849
- monitoring.reportError('redis', error);
2850
-
2851
- // Check if connection is recoverable
2852
- if (helper.client.status === 'end') {
2853
- // Connection is permanently closed
2854
- process.exit(1);
2855
- }
2856
- // Otherwise, ioredis will auto-reconnect per retryStrategy
2857
- },
2858
- });
2859
- ```
2860
-
2861
- ### Queue Helpers
2862
-
2863
- BullMQ worker failures are handled per-job:
2864
-
2865
- ```typescript
2866
- const worker = BullMQHelper.newInstance({
2867
- // ...
2868
- onWorkerDataFail: async (job, error) => {
2869
- // job may be undefined if the failure happened before job was fetched
2870
- if (job) {
2871
- console.error(`Job ${job.id} failed (attempt ${job.attemptsMade}):`, error);
2872
- }
2873
- // BullMQ automatically retries based on job options (attempts, backoff)
2874
- },
2875
- });
2876
- ```
2877
-
2878
- ### Kafka Helpers
2879
-
2880
- Kafka helpers use try/catch with re-throw. All operations log the error before throwing:
2881
-
2882
- ```typescript
2883
- try {
2884
- await producer.send({ messages: [...] });
2885
- } catch (error) {
2886
- // The helper already logged:
2887
- // "[send] Failed to send messages: <error> | ID: <identifier>"
2888
- // The onError callback was also invoked
2889
- // The error is re-thrown for your handling
2890
- }
2891
- ```
2892
-
2893
- ### Storage Helpers
2894
-
2895
- Storage operations throw `ApplicationError` for validation failures and missing resources:
2896
-
2897
- ```typescript
2898
- try {
2899
- await storage.upload({ bucket: 'nonexistent', files: [...] });
2900
- } catch (error) {
2901
- // ApplicationError: "[upload] Bucket does not exist | name: nonexistent"
2902
- }
2903
-
2904
- try {
2905
- await storage.createBucket({ name: '../etc/passwd' });
2906
- } catch (error) {
2907
- // ApplicationError: "[createBucket] Invalid name to create bucket!"
2908
- }
2909
- ```
2910
-
2911
- ---
2912
-
2913
- ## Performance Tips
2914
-
2915
- ### Redis Connection Pooling
2916
-
2917
- - Each Redis helper instance creates a single IoRedis connection by default. For high-throughput, consider multiple instances with different roles (read vs write).
2918
- - BullMQ calls `.duplicate()` automatically -- it does not share your main connection.
2919
- - The `maxRetriesPerRequest: null` setting prevents blocking on failed requests.
2920
- - Use `autoConnect: false` (lazy connect) when you need to control connection timing.
2921
-
2922
- ### Logger Performance
2923
-
2924
- - `Logger.get()` caching avoids creating new Winston loggers per call.
2925
- - The `debug()` method has zero overhead when `DEBUG` is not set (check is pre-computed at module load).
2926
- - For hot paths (>10k calls/sec), use `HfLogger` instead of Winston (~100--300ns vs ~1--10us).
2927
- - Consider disabling file transports in development for faster startup.
2928
-
2929
- ### Queue Optimization
2930
-
2931
- - In-memory `QueueHelper` processes one item at a time (sequential). For parallel processing, use BullMQ with `numberOfWorker > 1`.
2932
- - BullMQ default job options (`removeOnComplete: true`, `removeOnFail: true`) prevent Redis memory growth.
2933
- - For high-throughput Kafka, tune `highWaterMark` and `maxWaitTime` based on your message sizes.
2934
-
2935
- ### Storage
2936
-
2937
- - MinIO `listObjects` returns a stream internally -- the helper collects all results. Use `maxKeys` to limit memory usage.
2938
- - Disk storage validates names before I/O operations, preventing unnecessary filesystem calls.
2939
- - Use `normalizeNameFn` to control file naming and avoid repeated sanitization.
2940
-
2941
- ### Crypto
2942
-
2943
- - AES-256-GCM is recommended over CBC for new code (built-in authentication).
2944
- - RSA key generation is expensive (~50--200ms for 2048-bit). Generate keys once and store them.
2945
- - ECDH uses Web Crypto API (`crypto.subtle`) which is hardware-accelerated on most platforms.
2946
- - Pre-encode HfLogger messages and ECDH keys at initialization time, not in hot paths.
2947
-
2948
- ### Network
2949
-
2950
- - `NodeFetchNetworkRequest` has zero dependencies but no interceptor support.
2951
- - `AxiosNetworkRequest` supports interceptors but adds the `axios` dependency.
2952
- - TCP clients with `reconnect: true` use a 5-second fixed delay between reconnection attempts.
2953
-
2954
- ---
2955
-
2956
- ## About IGNIS
2957
-
2958
- IGNIS brings together the structured, enterprise development experience of **LoopBack 4** with the blazing speed and simplicity of **Hono** -- giving you the best of both worlds.
2959
-
2960
- ## Documentation
2961
-
2962
- - [IGNIS Repository](https://github.com/VENIZIA-AI/ignis)
2963
- - [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/get-started/5-minute-quickstart.md)
2964
- - [Reference Index](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/index.md)
2965
-
2966
- ## License
2967
-
2968
- [MIT](./LICENSE.md)
91
+ ```bash
92
+ # example: pino logging + BullMQ jobs + MinIO storage
93
+ bun add pino bullmq minio
94
+ ```
95
+
96
+ Everything else - Redis (`ioredis` is a real dependency), disk and in-memory storage, crypto,
97
+ network, UID, env, pool, worker threads - comes straight off the root barrel.
98
+
99
+ ## Modules
100
+
101
+ | Module | What it gives you |
102
+ | :--- | :--- |
103
+ | [logger](https://ignis.venizia.ai/extensions/helpers/logger/) | Scoped `ILogger` over a pluggable winston or pino backend, plus `HfLogger` for hot paths |
104
+ | [redis](https://ignis.venizia.ai/extensions/helpers/redis/) | Single, cluster, and sentinel clients over `ioredis`: key-value, hash, RedisJSON, pub/sub |
105
+ | [queue](https://ignis.venizia.ai/extensions/helpers/queue/) | BullMQ jobs, MQTT topics, [Kafka](https://ignis.venizia.ai/extensions/helpers/kafka/) producers and consumers, plus an in-process `SequentialQueueHelper` |
106
+ | [storage](https://ignis.venizia.ai/extensions/helpers/storage/) | One `IStorageHelper` interface over MinIO/S3, Bun S3, local disk, and memory |
107
+ | [crypto](https://ignis.venizia.ai/extensions/helpers/crypto/) | `AES`, `RSA`, `ECDH` - encryption, signing, and key exchange |
108
+ | [cron](https://ignis.venizia.ai/extensions/helpers/cron/) | `CronHelper`: declarative schedules with lifecycle hooks |
109
+ | [socket](https://ignis.venizia.ai/extensions/helpers/socket-io/) | Socket.IO server and client, plus a [Bun-native WebSocket](https://ignis.venizia.ai/extensions/helpers/websocket/) server |
110
+ | [network](https://ignis.venizia.ai/extensions/helpers/network/) | HTTP fetchers (native or axios) and TCP, TLS, UDP clients and servers |
111
+ | [uid](https://ignis.venizia.ai/extensions/helpers/uid/) | `SnowflakeUidHelper`: 70-bit time-sortable IDs with Base62 encoding, ~4M/sec/worker |
112
+ | [env](https://ignis.venizia.ai/extensions/helpers/env/) | `ApplicationEnvironment`: prefixed, typed access to `process.env` |
113
+ | pool | `BasePoolHelper`: generic resource pooling with an O(1) FIFO wait queue |
114
+ | [secrets](https://ignis.venizia.ai/extensions/helpers/secrets/) | `createSecretsHelper`: system envs, HashiCorp Vault, or dotenvx behind one interface |
115
+ | [worker-thread](https://ignis.venizia.ai/extensions/helpers/worker-thread/) | `WorkerPoolHelper` and a typed message bus for CPU-bound work |
116
+
117
+ Also on the root barrel: [error](https://ignis.venizia.ai/extensions/helpers/error/) helpers
118
+ (`getError`, `ApplicationError`, `isApplicationError`) and utilities for dates, parsing, promises,
119
+ retries, and requests.
120
+
121
+ ## Logging
122
+
123
+ Consumers type against **`ILogger`** and never name a concrete logger class. Get one three ways:
124
+
125
+ | Source | When |
126
+ | :--- | :--- |
127
+ | `this.logger` | Inside anything extending `BaseHelper` - already scoped |
128
+ | `LoggerFactory.getLogger(['Service', 'method'])` | Anywhere; scopes join with `-` |
129
+ | `ApplicationLogger.get('MyService')` | Single-scope shorthand |
130
+
131
+ `logger.for('methodName')` returns a child logger scoped to that method.
132
+
133
+ **Exactly five levels**, in severity order: `debug`, `info`, `warn`, `error`, `emerg`. `debug()` is
134
+ gated once at module load by `DEBUG` plus an `NODE_ENV` allowlist.
135
+
136
+ **Provider selection** happens once, at the entrypoint, through `LoggerFactory.use({ provider })`.
137
+ The factory hands out stable wrappers that resolve their backend lazily, so loggers acquired before
138
+ `use()` re-point to the new provider. With no provider registered, winston loads on the first log
139
+ call.
140
+
141
+ > [!IMPORTANT]
142
+ > Compiled binaries (`bun build --compile`) **must** call `LoggerFactory.use()` explicitly. The
143
+ > winston fallback is a runtime `require`, which a bundler cannot see - only a class reference
144
+ > carries a provider into a bundle.
145
+
146
+ `HfLogger` is a separate ring-buffered logger for hot paths where allocation matters, drained by
147
+ `HfLogFlusher`. Reach for it when profiling says to, not before.
148
+
149
+ ### Environment variables
150
+
151
+ | Variable | Default | Effect |
152
+ | :--- | :--- | :--- |
153
+ | `APP_ENV_LOGGER_LEVEL` | `debug` | Floor below which a line reaches no transport |
154
+ | `APP_ENV_LOGGER_FORMAT` | `text` (winston) / `json` (pino) | `json` or `text`; pino needs `pino-pretty` for `text` |
155
+ | `APP_ENV_LOGGER_FOLDER_PATH` | unset | Set it to enable rotating file output; unset means console or stdout only |
156
+ | `APP_ENV_LOGGER_FILE_FREQUENCY` | `1h` | Rotation interval |
157
+ | `APP_ENV_LOGGER_FILE_MAX_SIZE` | `100m` | Size at which a file rotates |
158
+ | `APP_ENV_LOGGER_FILE_MAX_FILES` | `5d` | Retention |
159
+ | `APP_ENV_LOGGER_FILE_DATE_PATTERN` | `YYYYMMDD_HH` | Filename date pattern (winston only) |
160
+ | `APP_ENV_LOGGER_DGRAM_HOST` / `_PORT` / `_LABEL` / `_LEVELS` | unset | UDP transport for remote log shipping (winston only) |
161
+ | `APP_ENV_LOGGER_DO_REDACT` | `true` | Set `false` to stop redacting secret-looking keys |
162
+ | `APP_ENV_LOGGER_INSPECT_DEPTH` | `5` | Depth before a nested object collapses to `[Object]` |
163
+ | `DEBUG` + `NODE_ENV` | unset | Together gate `debug()`; read once at module load |
164
+
165
+ Full detail: [Logger reference](https://ignis.venizia.ai/extensions/helpers/logger/).
166
+
167
+ ## Requirements
168
+
169
+ - **Bun >= 1.3.** Node.js 18+ runs most modules; the Bun-native ones (`/bun-s3`, the WebSocket
170
+ server) need Bun.
171
+ - `experimentalDecorators` and `emitDecoratorMetadata` must be `true` in your `tsconfig.json`,
172
+ declared **inline** - Bun does not resolve them through `extends`. Copy them from
173
+ `@venizia/dev-configs/tsconfig.common.json`.
174
+ - Errors always go through `getError` / `ApplicationError`, never `new Error`. Across package
175
+ boundaries check with `isApplicationError()`, never `instanceof` - the class has more than one
176
+ identity in a monorepo.
177
+
178
+ ## Links
179
+
180
+ [Helpers documentation](https://ignis.venizia.ai/extensions/helpers/) &#8226;
181
+ [All extensions](https://ignis.venizia.ai/extensions/) &#8226;
182
+ [Best practices](https://ignis.venizia.ai/best-practices/) &#8226;
183
+ [Changelog](https://ignis.venizia.ai/changelogs/) &#8226;
184
+ [Issues](https://github.com/VENIZIA-AI/ignis/issues)
185
+
186
+ MIT licensed - see [LICENSE.md](LICENSE.md). Questions: developer@venizia.ai
187
+ </content>