@venizia/ignis-docs 0.2.1-0 → 0.2.1-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -20,13 +20,33 @@ throw getError({
20
20
  });
21
21
  ```
22
22
 
23
- The framework's `AppErrorMiddleware` catches `ApplicationError` instances and formats them into a consistent JSON response - see [Common tasks](#common-tasks) below.
23
+ The framework's `AppErrorMiddleware` catches `ApplicationError` instances and formats them into a consistent JSON response.
24
+
25
+ ## Find what you need
26
+
27
+ | You want to | Go to |
28
+ |---|---|
29
+ | Throw a one-off error with a status code | [Throw a free-form error](#throw-a-free-form-error) |
30
+ | Re-throw an error you already caught and shaped | [Recognize an already-shaped error in a catch block](#recognize-an-already-shaped-error-in-a-catch-block) |
31
+ | Declare a reusable, i18n-ready error once | [Catalog a reusable error](#catalog-a-reusable-error) |
32
+ | Get autocomplete on `messageCode` | [Register catalog keys for messageCode autocomplete](#register-catalog-keys-for-messagecode-autocomplete) |
33
+ | Build a message code outside a catalog | [Build a code outside a catalog](#build-a-code-outside-a-catalog) |
34
+ | See the exact JSON shape a client receives | [Read the error response shape](#read-the-error-response-shape) |
35
+ | Read or rehydrate a server error in a browser app | [Consume the error response from a browser client](#consume-the-error-response-from-a-browser-client) |
36
+ | Look up what a specific input produces | [Every shape and its output](#every-shape-and-its-output) |
37
+ | Confirm you're checking errors the safe way | [How it works](#how-it-works) - `isApplicationError()`, never `instanceof` |
24
38
 
25
39
  ## How it works
26
40
 
27
- - **Three equivalent entry points.** `getError(opts)`, `new ApplicationError(opts)`, and the static `ApplicationError.getError(opts)` all take the same input and build the same object - use the class form only when a direct reference reads better.
28
- - **Two input shapes.** Free-form (`{ message, statusCode?, messageCode? }`) covers one-off failures - most throw sites. Catalogued (`{ error: TErrorDefinition }`) raises a failure declared once at module scope, so its code, status, and default text cannot drift across the call sites that raise it.
29
- - **One message shape everywhere.** `message` is either the historical string (with sibling `messageCode?`/`messageArgs?`) or an object mirroring `normalized`: `{ text, code?, args? }`. Both resolve to the same `normalized`:
41
+ - **Three equivalent entry points.** `getError(opts)`, `new ApplicationError(opts)`, and the static `ApplicationError.getError(opts)` all build the same object from the same input. Use the class form only when a direct reference reads better.
42
+ - **Two input shapes.** Free-form covers one-off failures - most throw sites. Catalogued raises a failure declared once at module scope, so its code, status, and default text can't drift across the call sites that raise it.
43
+
44
+ | Shape | Input |
45
+ |---|---|
46
+ | Free-form | `{ message, statusCode?, messageCode? }` |
47
+ | Catalogued | `{ error: TErrorDefinition }` |
48
+
49
+ - **One message shape everywhere.** `message` is either the historical string (with sibling `messageCode?`/`messageArgs?`), or an object mirroring `normalized`: `{ text, code?, args? }`. Both resolve to the same `normalized`:
30
50
 
31
51
  ```typescript
32
52
  getError({ message: 'Only %{n} left', messageCode: 'stock.low', messageArgs: { n: 2 } }); // flat
@@ -34,13 +54,20 @@ The framework's `AppErrorMiddleware` catches `ApplicationError` instances and fo
34
54
  getError({ error: StockErrors.LOW, messageArgs: { n: 2 } }); // catalogued
35
55
  ```
36
56
 
37
- On the catalogued form, `message` is a **partial** override - `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`.
38
- - **Precedence, most specific first.** `code`: `message.code` -> the definition's `message.code` -> `messageCode`. `args`: `message.args` -> `messageArgs` -> the definition's `message.args`.
39
- - **`messageCode` always resolves to something.** `MessageCode.resolve()` lower-cases it and falls back to `MessageCode.DEFAULT` (`'core.system_error'`) when none is given or it is empty - `error.normalized.code` is never `undefined`.
40
- - **`normalized` is the single source, and there is no flat duplicate.** `{ text, code, args }` - `text` defaults to `message`, `code`/`args` resolve per the precedence above. `args` is always populated (`{}` when empty), so no consumer needs a null check. A client renders any error with one lookup: `translate(error.normalized.code, error.normalized.args)`. `messageCode` and `messageArgs` are INPUTS only: there is no `error.messageCode` field, and `extra` never mirrors `messageArgs`. Pass `transform` to build `normalized` yourself in place of the default.
41
- - **Any key the input does not declare rides into `extra`.** Attach whatever context your clients need - `getError({ message, transaction: {...} })` lands at `error.extra.transaction`. Passing `extra` explicitly works too, and the two merge with the explicit one winning. `extra` carries caller context ONLY; it is `undefined` when there is nothing to carry.
42
- - **`cause` reaches the native `Error.cause`,** not `extra` - wrap a lower-level failure with `getError({ message, cause: originalError })` and every tool that reads `.cause` sees it. `error` is **refused** on the free-form branch (`error?: never`) precisely so this mistake fails at compile time: `getError({ message, error: caughtError })` does not compile - use `cause`.
43
- - **`isApplicationError()` checks shape, not class identity.** There is one `ApplicationError` - it lives in `@venizia/ignis-inversion` so a browser application can raise and read the same errors the server does, and `helpers` re-exports it. `instanceof` still fails across a package boundary: inversion ships dual CJS+ESM builds, so one source class has two runtime constructors. Test the shape.
57
+ On the catalogued form, `message` is a **partial** override. `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`.
58
+
59
+ - **Precedence resolves most-specific-first.** See the [full precedence table](#every-shape-and-its-output) below - a definition's own `message.code` beats a flat `messageCode`.
60
+ - **`messageCode` always resolves to something.** `MessageCode.resolve()` lower-cases it and falls back to `MessageCode.DEFAULT` (`'core.system_error'`) when none is given or empty. `error.normalized.code` is never `undefined`.
61
+ - **`normalized` is the single source - there's no flat duplicate.** It's `{ text, code, args }`. `text` defaults to `message`; `code` and `args` resolve per the precedence table.
62
+ - **`args` is always populated, so no consumer needs a null check.** One lookup renders any error: `translate(error.normalized.code, error.normalized.args)`.
63
+ - **`messageCode` and `messageArgs` are INPUTS only.** There's no `error.messageCode` field, and `extra` never mirrors `messageArgs`. Pass `transform` to build `normalized` yourself in place of the default.
64
+ - **Any key the input doesn't declare rides into `extra`.** Attach whatever context your clients need: `getError({ message, transaction: {...} })` lands at `error.extra.transaction`. Passing `extra` explicitly works too - the two merge, and the explicit one wins on a clash.
65
+ - **`extra` carries caller context only.** It's `undefined` when there is nothing to carry.
66
+ - **`cause` reaches the native `Error.cause`, not `extra`.** Wrap a lower-level failure with `getError({ message, cause: originalError })`, and every tool that reads `.cause` sees it.
67
+ - **`error` is refused on the free-form branch (`error?: never`).** This makes `getError({ message, error: caughtError })` fail to compile, so the mistake is caught before runtime - use `cause` instead.
68
+
69
+ > [!IMPORTANT]
70
+ > **Use `isApplicationError()`, never `instanceof ApplicationError`.** There is one `ApplicationError` class, defined in `@venizia/ignis-inversion`. `helpers` re-exports it, so a browser app can raise and read the same errors the server does. But `instanceof` still fails across a package boundary: inversion ships dual CJS+ESM builds, so one source class has two runtime constructors. `isApplicationError()` tests shape, not identity, so it works everywhere.
44
71
 
45
72
  **Options shared by both forms**
46
73
 
@@ -53,10 +80,11 @@ The framework's `AppErrorMiddleware` catches `ApplicationError` instances and fo
53
80
  | `cause` | `unknown` | The wrapped failure - reaches `Error.cause`. Use this to wrap a caught error; `error` is refused on the free-form branch for exactly this case |
54
81
  | `extra` | `Record<string, unknown>` | Explicit context, merged with any swept keys - and it wins on a clash |
55
82
  | `transform` | `TErrorNormalizeTransformFn` | Builds `normalized` in place of the default. Receives `{ message: TErrorNormalized, statusCode, extra? }` - `message` is the default normalized being replaced, so `s => ({ ...s.message, text: 'x' })` amends one field |
83
+ | `logLevel` | `error \| emerg \| warn \| info \| debug` | The level the error handler logs this at. Defaults to `error`; lower it for an expected failure (`getError({ message, statusCode: 404, logLevel: 'warn' })`), or raise it to `emerg`. Steers the server log only - never the response |
56
84
  | _anything else_ | `unknown` | Rides into `extra` under its own name. `getError({ message, transaction })` lands at `error.extra.transaction` |
57
85
 
58
86
  > [!TIP]
59
- > Spreading a definition now resolves identically to passing it as `error`: `getError({ ...CategoryErrors.CREATE_DUPLICATE_NAME })` and `getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME })` build the same `ApplicationError`. A definition's `message` is `{ text, code, args? }` - the same shape the free-form input accepts - so the spread degrades to nothing. Prefer `{ error: DEF }` anyway; it reads as "raise this catalogued error" instead of "raise these loose fields."
87
+ > Spreading a definition now resolves identically to passing it as `error`: `getError({ ...CategoryErrors.CREATE_DUPLICATE_NAME })` and `getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME })` build the same `ApplicationError`. A definition's `message` is `{ text, code, args? }` - the same shape the free-form input accepts - so the spread degrades to nothing. Prefer `{ error: DEF }` anyway. It reads as "raise this catalogued error", not "raise these loose fields."
60
88
 
61
89
  ## Every shape and its output
62
90
 
@@ -190,14 +218,14 @@ throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, messageArgs: { nam
190
218
  throw getError({ error: CategoryErrors.CREATE_DUPLICATE_NAME, message: { args: { name: 'Vé' } } });
191
219
  ```
192
220
 
193
- `ErrorScopes` groups a failure by intent - `AUTH`, `VALIDATION`, `BUSINESS`, `SYSTEM`, `INTEGRATION` - because `statusCode` cannot: a `409` is a business conflict in one place and a validation clash in another.
221
+ `ErrorScopes` groups a failure by intent - `AUTH`, `VALIDATION`, `BUSINESS`, `SYSTEM`, `INTEGRATION`. `statusCode` can't do this job: a `409` is a business conflict in one place and a validation clash in another.
194
222
 
195
223
  > [!WARNING]
196
- > `category` is catalog **metadata only** - it does not reach the error response. `getError` reads `message.text`, `message.code` and `statusCode` off a definition and ignores the rest. Use it to group and filter catalogs (ops dashboards, translator exports); do not expect a client to receive it.
224
+ > `category` is catalog **metadata only** - it does not reach the error response. `getError` reads `message.text`, `message.code` and `statusCode` off a definition and ignores the rest. Use it to group and filter catalogs (ops dashboards, translator exports). Don't expect a client to receive it.
197
225
 
198
226
  ### Register catalog keys for `messageCode` autocomplete
199
227
 
200
- Augment the module the file already imports from. `IErrorKeyRegistry` is declared in `@venizia/ignis-inversion` and re-exported by `helpers`; merging follows the re-export, so either name populates the same registry.
228
+ Augment the module the file already imports from. `IErrorKeyRegistry` is declared in `@venizia/ignis-inversion` and re-exported by `helpers`. Merging follows the re-export, so either name populates the same registry.
201
229
 
202
230
  ```typescript
203
231
  import type { TRegisterErrors } from '@venizia/ignis-helpers';
@@ -225,7 +253,7 @@ const NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
225
253
 
226
254
  ### Read the error response shape
227
255
 
228
- `AppErrorMiddleware` (from `@venizia/ignis`) routes every thrown value into one of five shapes. All five carry `message`, `statusCode`, `normalized` and `details`; only an intentional error can carry `extra`. The code lives at `normalized.code` - there is no flat `messageCode` on the response.
256
+ `AppErrorMiddleware` (from `@venizia/ignis`) routes every thrown value into one of five shapes. All five carry `message`, `statusCode`, `normalized` and `details`. Only an intentional error can carry `extra`. The code lives at `normalized.code` - there is no flat `messageCode` on the response.
229
257
 
230
258
  | What was thrown | Status | `normalized.code` | `message` | `extra` |
231
259
  | ----------------------------------------- | ------- | ------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------- |
@@ -235,11 +263,11 @@ const NOT_FOUND = MessageCode.build({ parts: ['core', 'user', 'not_found'] });
235
263
  | `getError(...)` - intentional | its own | its own | its own | when the throw site attached `extra` or unknown keys |
236
264
  | Anything else | 500 | `core.system_error` | `Internal Server Error` in production; the raw message in development | never |
237
265
 
238
- Only the intentional branch reports what the throw site wrote. The other four **replace** the message, because a driver error carries SQL, schema and constraint names - and `normalized` is built from the replacement, so it can never leak what `message` just scrubbed.
266
+ Only the intentional branch reports what the throw site wrote. The other four **replace** the message - a driver error can carry SQL, schema, and constraint names. `normalized` is built from that replacement, so it can never leak what `message` just scrubbed.
239
267
 
240
268
  `rootKey` (e.g. `new AppErrorMiddleware({ logger, rootKey: 'error' }).value()`) nests any of these under that key.
241
269
 
242
- The handler is fail-closed on environment: it exposes `stack` and `cause` in `details` only when `NODE_ENV` is one of `local`, `debug`, `development`, `dev`, `sit` (`Environment.DEVELOPMENT_ENVS`). Everything else - including `alpha`, `staging`, a typo, or an unset `NODE_ENV` - gets the sanitized shape:
270
+ The handler is fail-closed on environment. It exposes `stack` and `cause` in `details` only for `Environment.DEVELOPMENT_ENVS`: `local`, `debug`, `development`, `dev`, `sit`. Everything else - including `alpha`, `staging`, a typo, or an unset `NODE_ENV` - gets the sanitized shape:
243
271
 
244
272
  ```json
245
273
  {
@@ -263,10 +291,58 @@ The handler is fail-closed on environment: it exposes `stack` and `cause` in `de
263
291
  > [!IMPORTANT]
264
292
  > **`messageCode` and `extra.messageArgs` are GONE from the response.** They duplicated `normalized.code` and `normalized.args`. Read `normalized` - it is the only source. A client still reading either must migrate: `translate(error.messageCode, error.extra?.messageArgs)` becomes `translate(error.normalized.code, error.normalized.args)`.
265
293
  >
266
- > Note the two `details`: the inner one is context the throw site attached (it went through `extra`), the outer one is the middleware's own request info. They are unrelated despite the name.
294
+ > Note the two `details`. The inner one is context the throw site attached - it went through `extra`. The outer one is the middleware's own request info. They're unrelated despite sharing a name.
267
295
 
268
296
  > [!NOTE]
269
- > `message` and `normalized.text` are the same string unless a `transform` deliberately makes them differ - `message` stays the raw text the throw site wrote, `normalized.text` is what a client shows. Most errors never set `transform`, so most of the time they match.
297
+ > `message` and `normalized.text` are the same string, unless a `transform` deliberately makes them differ. `message` stays the raw text the throw site wrote. `normalized.text` is what a client shows. Most errors never set `transform`, so most of the time the two match.
298
+
299
+ ### Consume the error response from a browser client
300
+
301
+ The error layer lives in `@venizia/ignis-inversion`, not in helpers, precisely so a browser app can
302
+ share it. It depends only on `lodash`, ships dual CJS+ESM, and pulls in no server module. A frontend
303
+ throws its own failures with the same `getError` the server uses, and reads the server's with the
304
+ same field names.
305
+
306
+ **Reading is all most clients need.** `normalized` is on every error response:
307
+
308
+ - `getError` always builds one.
309
+ - A foreign error gets one synthesized.
310
+ - A `ZodError` builds its own.
311
+
312
+ So there's no null-check and no parsing step: one lookup handles every case.
313
+
314
+ ```ts
315
+ const { error } = await response.json(); // `error` is the rootKey, if one is configured
316
+
317
+ translate(error.normalized.code, error.normalized.args);
318
+ ```
319
+
320
+ **Rehydrating** is for the client that wants one `catch` block for both a server failure and a
321
+ locally thrown one. `fromError` inverts the response the middleware emitted:
322
+
323
+ ```ts
324
+ import { fromError, isApplicationError } from '@venizia/ignis-inversion';
325
+
326
+ const { error } = await response.json();
327
+ throw fromError({ error }); // now an ApplicationError - isApplicationError() is true
328
+ ```
329
+
330
+ | Wire field | Where it lands |
331
+ | ------------ | --------------------------------------------------------------------------------- |
332
+ | `normalized` | `normalized`, verbatim - `text`, `code` and `args` round-trip unchanged |
333
+ | `message` | `normalized.text`, but only when `normalized` is missing entirely |
334
+ | `statusCode` | `statusCode`; `400` when the payload carries none |
335
+ | `extra` | `extra`, verbatim |
336
+ | `requestId` | `extra.requestId` - it is the identifier a support ticket quotes, so it survives |
337
+ | `details` | dropped - `url` and `path` the client already knows, and `stack` is the server's |
338
+
339
+ Every field of `TResponsedError` is optional by design: a client parses what a gateway, a proxy or an
340
+ older server actually sent. A body that is not an IGNIS error at all still yields an
341
+ `ApplicationError`, degraded to `MessageCode.DEFAULT` and status `400`. No call site branches on
342
+ a parse failure.
343
+
344
+ > [!TIP]
345
+ > Same rule here: prefer `isApplicationError()` over `instanceof ApplicationError`. See the [callout above](#how-it-works) for why - a browser app bundles its own copy of the class, so `instanceof` fails across the boundary.
270
346
 
271
347
  **Common status codes**
272
348
 
@@ -290,8 +366,8 @@ The handler is fail-closed on environment: it exposes `stack` and `cause` in `de
290
366
  **Files:**
291
367
 
292
368
  - [`packages/helpers/src/modules/error/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/index.ts) - module barrel
293
- - [`packages/helpers/src/modules/error/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/types.ts) - `ErrorSchema`, `TErrorResponse` (the RESPONSE schema, for OpenAPI)
294
- - [`packages/inversion/src/modules/error/app-error.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/app-error.ts) - `ApplicationError`, `getError`, `isApplicationError`
295
- - [`packages/inversion/src/modules/error/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/types.ts) - `TError`, `TErrorDefinition`, `TErrorNormalized`, `IErrorKeyRegistry`, `TRegisterErrors`
369
+ - [`packages/helpers/src/modules/error/schemas.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/error/schemas.ts) - `ErrorSchema`, `TErrorResponse` (the RESPONSE schema, for OpenAPI)
370
+ - [`packages/inversion/src/modules/error/app-error.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/app-error.ts) - `ApplicationError`, `getError`, `fromError`, `isApplicationError`
371
+ - [`packages/inversion/src/modules/error/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/types.ts) - `TError`, `TErrorDefinition`, `TErrorNormalized`, `TResponsedError`, `IErrorKeyRegistry`, `TRegisterErrors`
296
372
  - [`packages/inversion/src/modules/error/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/definition.ts) - `ErrorScopes`, `TErrorScope`
297
373
  - [`packages/inversion/src/modules/error/message-code.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/inversion/src/modules/error/message-code.ts) - `MessageCode`
@@ -1,49 +1,63 @@
1
1
  # Helpers
2
2
 
3
- Reusable classes and functions providing common functionality - designed for easy injection and configuration.
4
-
5
- ## Available Helpers
6
-
7
- | Helper | Purpose | Key Features |
8
- |--------|---------|--------------|
9
- | [Common Types](./types/) | Utility types | Nullable, resolvers, class types |
10
- | [Cron](./cron/) | Job scheduling | Cron expressions, task management |
11
- | [Crypto](./crypto/) | Cryptographic operations | AES/RSA/ECDH encryption, key exchange, hashing |
12
- | [Environment](./env/) | Environment variables | Centralized config access |
13
- | [Error](./error/) | Error handling | `ApplicationError`, consistent responses |
14
- | [Inversion](./inversion/) | Dependency injection | IoC container - separate package `@venizia/ignis-inversion` |
15
- | [Logger](./logger/) | Logging | Winston-based, multiple transports, scopes |
16
- | [Network](./network/) | Network requests | HTTP, TCP, UDP helpers |
17
- | [Kafka](./kafka/) | Event streaming | Apache Kafka producer/consumer/admin/schema registry |
18
- | [Queue](./queue/) | Message queues | BullMQ, MQTT support |
19
- | [Redis](./redis/) | Redis operations | Single/cluster, key-value, hashes, JSON, pub/sub |
20
- | [Socket.IO](./socket-io/) | Real-time communication | Socket.IO client/server helpers |
21
- | [WebSocket](./websocket/) | Real-time communication | Bun native WebSocket server/emitter, Redis scaling |
22
- | [Storage](./storage/) | File storage | In-memory, disk, MinIO, Bun S3 object storage |
23
- | [UID](./uid/) | Unique ID generation | Snowflake IDs, Base62 encoding |
24
- | [Worker Thread](./worker-thread/) | Worker threads | Node.js worker management |
25
-
26
- ### Subpath Imports
27
-
28
- Some helpers with optional peer dependencies are only available via subpath imports to ensure proper tree-shaking:
29
-
30
- | Subpath | Peer Dependency | Description |
31
- |---------|-----------------|-------------|
32
- | `@venizia/ignis-helpers/kafka` | `@platformatic/kafka` | Kafka producer/consumer/admin/schema registry |
33
- | `@venizia/ignis-helpers/bullmq` | `bullmq` | BullMQ job queue |
34
- | `@venizia/ignis-helpers/mqtt` | `mqtt` | MQTT pub/sub client |
35
- | `@venizia/ignis-helpers/socket-io` | `socket.io`, `socket.io-client` | Socket.IO server/client |
36
- | `@venizia/ignis-helpers/minio` | `minio` | MinIO S3-compatible storage |
37
- | `@venizia/ignis-helpers/bun-s3` | -- | Bun native S3 storage |
38
- | `@venizia/ignis-helpers/cron` | `cron` | Cron job scheduling |
39
- | `@venizia/ignis-helpers/axios` | `axios` | Axios HTTP client |
40
-
41
- ## See Also
42
-
43
- - **Related Concepts:**
44
- - [Services](/guides/core-concepts/services) - Using helpers in service layer
45
- - [Controllers](/guides/core-concepts/rest-controllers) - Using helpers in controllers
46
-
47
- - **References:**
48
- - [Utilities](/references/utilities/) - Pure utility functions
49
- - [Components](/extensions/components/) - Framework components
3
+ Standalone utility classes for infrastructure concerns - extend `BaseHelper` for scoped logging, and inject them wherever you need them.
4
+
5
+ Every peer dependency below is optional. You install one only when you use the helper that needs it.
6
+
7
+ | Helper | What it does | When you reach for it | Peer dependency |
8
+ |---|---|---|---|
9
+ | [Types](./types/) | Shared utility types | You need IGNIS's shared TypeScript utility types | None |
10
+ | [Cron](./cron/) | Scheduled tasks | You run code on a cron schedule | `cron` |
11
+ | [Crypto](./crypto/) | Encryption and signing | You hash, encrypt, or sign data | None |
12
+ | [Environment](./env/) | Env var management | You need typed, validated env var access | None |
13
+ | [Error](./error/) | Error utilities | You throw or handle an error | None |
14
+ | [Secrets & Vault](./secrets/) | Secrets and credentials | You read config or credentials from Vault or a vaulted `.env` | `node-vault` or `@dotenvx/dotenvx` |
15
+ | [Inversion](./inversion/) | DI container | You build custom bindings or providers | None |
16
+ | [Logger](./logger/) | Logging | You need scoped, leveled logging | `winston` or `pino` |
17
+ | [Network](./network/) | HTTP/TCP/UDP clients | You call another service over HTTP, TCP, or UDP | `axios`, for the Axios client only |
18
+ | [Kafka](./kafka/) | Kafka messaging | You publish or consume Kafka topics | `@platformatic/kafka` |
19
+ | [Queue](./queue/) | Job queues | You need background or delayed work | `bullmq` or `mqtt` |
20
+ | [Redis](./redis/) | Redis client | You need a Redis connection - cache, pub/sub, locks | None - `ioredis` ships with the package |
21
+ | [Socket.IO](./socket-io/) | Socket.IO server | You build a custom real-time feature | `socket.io` |
22
+ | [WebSocket](./websocket/) | WebSocket server | You build a custom real-time feature | None |
23
+ | [Storage](./storage/) | File storage | You read/write files to MinIO or disk directly | `minio`, for the MinIO backend only |
24
+ | [UID](./uid/) | Snowflake IDs | You need unique, sortable IDs | None |
25
+ | [Worker Thread](./worker-thread/) | Worker pools | You move CPU-heavy work off the main thread | None |
26
+
27
+ ## Subpath imports
28
+
29
+ A helper with an optional peer dependency ships from its own subpath, so a bundler never pulls in a peer you don't use.
30
+
31
+ | Import from | Requires |
32
+ |---|---|
33
+ | `@venizia/ignis-helpers/cron` | `cron` |
34
+ | `@venizia/ignis-helpers/axios` | `axios` |
35
+ | `@venizia/ignis-helpers/kafka` | `@platformatic/kafka` |
36
+ | `@venizia/ignis-helpers/bullmq` | `bullmq` |
37
+ | `@venizia/ignis-helpers/mqtt` | `mqtt` |
38
+ | `@venizia/ignis-helpers/socket-io` | `socket.io`, `socket.io-client` |
39
+ | `@venizia/ignis-helpers/minio` | `minio` |
40
+ | `@venizia/ignis-helpers/bun-s3` | none - Bun native |
41
+ | `@venizia/ignis-helpers/hashicorp-vault` | `node-vault` |
42
+ | `@venizia/ignis-helpers/dotenv-vault` | `@dotenvx/dotenvx` |
43
+ | `@venizia/ignis-helpers/winston` | `winston` |
44
+ | `@venizia/ignis-helpers/pino` | `pino` |
45
+
46
+ `@venizia/ignis-helpers/common` is a subpath of a different kind. It isolates nothing - it exposes the
47
+ part of this package that is already browser-safe: `HTTP`, `TConstValue`, and the constant and
48
+ redaction tables. Import it when your code has to bundle for a browser.
49
+
50
+ The root barrel cannot: it re-exports every module, so reaching one constant through it pulls in 14
51
+ node builtins and 27 packages, `winston` and `ioredis` among them. A guard test bundles both entry
52
+ points for a browser target and fails if that ever changes.
53
+
54
+ ```typescript
55
+ import { HTTP } from '@venizia/ignis-helpers/common';
56
+ ```
57
+
58
+ ## See also
59
+
60
+ - [Services](/guides/core-concepts/services) - using helpers in the service layer
61
+ - [Controllers](/guides/core-concepts/rest-controllers) - using helpers in controllers
62
+ - [Utilities](/references/utilities/) - pure utility functions
63
+ - [Components](/extensions/components/) - framework components
@@ -29,14 +29,29 @@ container.bind<UserService>({ key: 'services.UserService' })
29
29
  const userService = container.get<UserService>({ key: 'services.UserService' });
30
30
  ```
31
31
 
32
- A class needs no decorator to be bindable - the binding is always created explicitly with `container.bind()`, and scope is set on the binding via `setScope()`. The framework layer (`@venizia/ignis`) creates these bindings for you for controllers, services, and repositories via `app.controller()` / `app.service()` / `@repository`.
32
+ A class needs no decorator to be bindable. The binding is always created explicitly with `container.bind()`, and its scope is set via `setScope()`.
33
+
34
+ The framework layer (`@venizia/ignis`) creates these bindings for you - for controllers, services, and repositories - via `app.controller()` / `app.service()` / `@repository`.
33
35
 
34
36
  ## How it works
35
37
 
36
- - **Three ways to resolve a binding.** `toClass` (container instantiates with DI), `toValue` (returned as-is), `toProvider` (factory function or `IProvider` class). All `Binding` setters return `this`, so calls chain.
37
- - **Instantiation is two-phase.** Constructor injection runs first, reading `@inject` metadata by parameter index and passing resolved values as constructor args; property injection runs second, assigning each `@inject`-decorated property on the built instance. `container.resolve(cls)` and `container.instantiate(cls)` are the same method - `resolve` is an alias.
38
+ - **A binding resolves one of three ways.** All `Binding` setters return `this`, so calls chain.
39
+
40
+ | Resolver | Behavior |
41
+ |---|---|
42
+ | `toClass` | Container instantiates the class with full DI |
43
+ | `toValue` | Returns the value as-is |
44
+ | `toProvider` | Calls a factory function, or an `IProvider` class |
45
+
46
+ - **Instantiation is two-phase.** `container.resolve(cls)` and `container.instantiate(cls)` are the same method - `resolve` is an alias.
47
+
48
+ | Phase | What happens |
49
+ |---|---|
50
+ | 1. Constructor injection | Reads `@inject` metadata by parameter index, passes resolved values as constructor args |
51
+ | 2. Property injection | Assigns each `@inject`-decorated property on the built instance |
52
+
38
53
  - **Every constructor parameter must carry `@inject`.** The metadata array is index-keyed - an undecorated parameter leaves a hole the container has no way to fill. `instantiate()` refuses the class by name and parameter index rather than passing `undefined`.
39
- - **Namespaces auto-tag bindings.** A key like `services.UserService` tags the binding `services` automatically; `setTags()` adds more. `findByTag()` queries by tag, with an `exclude` list.
54
+ - **Namespaces auto-tag bindings.** A key like `services.UserService` tags the binding `services` automatically. `setTags()` adds more. `findByTag()` queries by tag, with an `exclude` list.
40
55
  - **Keys** can be a `string`, a `symbol`, or `{ namespace, key }` (built into a dotted string via `BindingKeys.build`).
41
56
 
42
57
  **Scopes**
@@ -47,9 +62,11 @@ A class needs no decorator to be bindable - the binding is always created explic
47
62
  | Singleton | `BindingScopes.SINGLETON` | Cached on the `Binding` after first resolution |
48
63
 
49
64
  > [!IMPORTANT]
50
- > Singleton caching lives on the `Binding` object, not the container. Rebinding a key creates a fresh `Binding` with its own cache; a `Binding` reference you hold onto keeps its own cache independent of `container.clear()`/`reset()` on a different `Binding` for the same key.
65
+ > Singleton caching lives on the `Binding` object, not the container. Rebinding a key creates a fresh `Binding` with its own cache. If you hold onto an old `Binding` reference, its cache stays independent of `container.clear()`/`reset()` calls made against the new `Binding` registered under the same key.
66
+
67
+ Property-injected classes only get their `@inject` properties populated when built through the container - `container.resolve()` or `instantiate()`. A plain `new MyClass()` leaves them `undefined`.
51
68
 
52
- Property-injected classes only get their `@inject` properties populated when built through the container (`container.resolve()`/`instantiate()`) - `new MyClass()` leaves them `undefined`. The [Full reference](/extensions/helpers/inversion/reference) covers `MetadataRegistry`, `gets()`, key formats, `IProvider`, and every error message in detail.
69
+ The [Full reference](/extensions/helpers/inversion/reference) covers `MetadataRegistry`, `gets()`, key formats, `IProvider`, and every error message in detail.
53
70
 
54
71
  ## Common tasks
55
72
 
@@ -80,7 +80,7 @@ import type {
80
80
  ```
81
81
 
82
82
  > [!NOTE]
83
- > The framework package `@venizia/ignis` re-exports DI-specific symbols (`Binding`, `BindingKeys`, `BindingScopes`, `BindingValueTypes`, `IProvider`, `isClass`, `isClassProvider`, `TBindingScope`, `TBindingValueType`, `IBindingTag`) from `@venizia/ignis-inversion` and adds higher-level helpers (`app.controller()`, `app.service()`, etc.). All types are also available via type-only re-exports.
83
+ > The framework package `@venizia/ignis` re-exports these DI symbols from `@venizia/ignis-inversion`, types included: `Binding`, `BindingKeys`, `BindingScopes`, `BindingValueTypes`, `IProvider`, `isClass`, `isClassProvider`, `TBindingScope`, `TBindingValueType`, `IBindingTag`. It also adds higher-level helpers of its own (`app.controller()`, `app.service()`, etc.).
84
84
 
85
85
  ## Class Hierarchy
86
86
 
@@ -92,7 +92,7 @@ AbstractContainer extends BaseHelper implements IContainer # contract only - e
92
92
  └── Container # instantiate() = two-phase decorator injection
93
93
  ```
94
94
 
95
- `AbstractContainer` exists so a container implementation that shares nothing with the shipped storage can start there; one that only wants to vary resolution extends `BaseContainer` instead. `binding/` and `container/` only talk to each other through `IContainer`/`IBinding` - there is no import cycle between the two folders.
95
+ `AbstractContainer` exists so a container implementation that shares nothing with the shipped storage can start there. One that only wants to vary resolution extends `BaseContainer` instead. `binding/` and `container/` only talk to each other through `IContainer`/`IBinding` - there is no import cycle between the two folders.
96
96
 
97
97
  ## Creating a Container
98
98
 
@@ -124,7 +124,7 @@ const container = new Container({ scope: 'MyApp' });
124
124
  | `reset` | `reset(): void` | Remove all bindings entirely |
125
125
  | `getMetadataRegistry` | `getMetadataRegistry(): MetadataRegistry` | Return the shared `metadataRegistry` singleton |
126
126
 
127
- All keys passed to `bind`, `isBound`, and `unbind` are normalized with `String(key)` before being used as the `Map` key - a `Symbol` key resolves to its `.toString()` form (`'Symbol(...)'`), so a `symbol` and the equivalent string are distinct entries.
127
+ All keys passed to `bind`, `isBound`, and `unbind` are normalized with `String(key)` before being used as the `Map` key. A `Symbol` key resolves to its `.toString()` form (`'Symbol(...)'`), so a `symbol` and the equivalent string are distinct entries.
128
128
 
129
129
  ### `instantiate()` - two-phase algorithm
130
130
 
@@ -139,10 +139,11 @@ override instantiate<T>(cls: TClass<T>): T
139
139
 
140
140
  The array is already index-keyed (`setInjectMetadata` writes to `injects[index]`) - there is no sort step. Once all arguments are resolved, `new cls(...args)` builds the instance.
141
141
 
142
- **Phase 2 - property injection.** Reads `registry.getPropertiesMetadata({ target: instance })`. If there is none, returns the instance as-is. Otherwise, for each `[propertyKey, metadata]` entry, resolves `this.get({ key: metadata.bindingKey, isOptional: metadata.optional ?? false })` and assigns it to `instance[propertyKey]`.
142
+ **Phase 2 - property injection.** Reads `registry.getPropertiesMetadata({ target: instance })`. If there is none, returns the instance as-is.
143
143
 
144
- > [!WARNING]
145
- > Property injection's optional check reads `metadata.optional`, but `@inject` writes `IPropertyMetadata.isOptional` (see [`IPropertyMetadata`](#metadataregistry-types) below) - `metadata.optional` is always `undefined` at runtime, so `metadata.optional ?? false` always evaluates to `false`. In practice, `@inject({ key, isOptional: true })` on a **property** has no effect: an unbound property dependency still throws. This is a verified source-level discrepancy in `container.ts`, not a documented API - `isOptional` on a **constructor parameter** (`meta.isOptional`, phase 1) works correctly. If you need an optional dependency, use `@inject` on a constructor parameter, or resolve it manually with `container.get({ key, isOptional: true })` inside the constructor.
144
+ Otherwise, for each `[propertyKey, metadata]` entry, resolves `this.get({ key: metadata.bindingKey, isOptional: metadata.isOptional ?? false })` and assigns it to `instance[propertyKey]`.
145
+
146
+ `@inject({ key, isOptional: true })` on a **property** behaves exactly like on a constructor parameter: an unbound key resolves to `undefined` instead of throwing. A required property (`isOptional` omitted or `false`) still throws when its key is unbound.
146
147
 
147
148
  ### Key formats
148
149
 
@@ -167,7 +168,7 @@ const [svcA, svcB] = container.gets<[ServiceA, ServiceB]>({
167
168
  });
168
169
  ```
169
170
 
170
- Internally maps each entry through `this.get({ ...opt, isOptional: true })` - regardless of what `isOptional` was set to on the entry, `gets()` always resolves with `isOptional: true` and returns `undefined` for anything unbound rather than throwing.
171
+ Internally maps each entry through `this.get({ ...opt, isOptional: true })`. Regardless of what `isOptional` was set on the entry, `gets()` always resolves with `isOptional: true` - anything unbound returns `undefined` instead of throwing.
171
172
 
172
173
  ## Binding
173
174
 
@@ -194,17 +195,20 @@ Internally maps each entry through `this.get({ ...opt, isOptional: true })` - re
194
195
  constructor(opts: { key: string })
195
196
  ```
196
197
 
197
- Splits `key` on `.`; if there is more than one segment, the first segment is auto-added as a tag via `setTags()`. `'services.UserService'` auto-tags `'services'`; a key with no `.` gets no automatic tag.
198
+ Splits `key` on `.`. If there is more than one segment, the first segment is auto-added as a tag via `setTags()`. `'services.UserService'` auto-tags `'services'`. A key with no `.` gets no automatic tag.
198
199
 
199
200
  ### `getValue()` resolution by type
200
201
 
201
202
  | Resolver type | Behavior | Throws when |
202
203
  |----------------|----------|--------------|
203
204
  | `VALUE` | Returns the stored value directly | Never |
204
- | `PROVIDER` | If the stored value is a plain function, calls `provider(container)`. If it is a class matching `isClassProvider` (prototype has a `value()` method), the container `instantiate()`s the class first, then calls `.value(container)` on the instance | No `container` argument was passed - `[getValue] Invalid context/container to get provider value` |
205
+ | `PROVIDER` (plain function) | Calls `provider(container)` | No `container` argument was passed - `[getValue] Invalid context/container to get provider value` |
206
+ | `PROVIDER` (class, matched via `isClassProvider`) | `container.instantiate()`s the class, then calls `.value(container)` on the instance | Same as above |
205
207
  | `CLASS` | `container.instantiate(this.resolver.value)` | No `container` argument was passed - `[getValue] Invalid context/container to instantiate class` |
206
208
 
207
- If `bindScope` is `SINGLETON`, the resolved instance is cached on `this.cached` and returned directly on every subsequent call without re-invoking the resolver - caching is per-`Binding` instance, not per-container.
209
+ `isClassProvider` matches a class whose prototype has a `value()` method - see [Class-based provider](#class-based-provider) below.
210
+
211
+ If `bindScope` is `SINGLETON`, the resolved instance is cached on `this.cached`. Every subsequent call returns it directly without re-invoking the resolver - caching is per-`Binding` instance, not per-container.
208
212
 
209
213
  ### Class-based provider
210
214
 
@@ -279,7 +283,6 @@ interface IInjectMetadata {
279
283
  interface IPropertyMetadata {
280
284
  bindingKey: TBindingKey;
281
285
  isOptional?: boolean;
282
- [key: string]: any;
283
286
  }
284
287
  ```
285
288
 
@@ -330,7 +333,7 @@ BindingKeys.build({ namespace: 'services', key: '' });
330
333
  // throws: [BindingKeys][build] Invalid key to build | key:
331
334
  ```
332
335
 
333
- `key` is required and must be non-empty; `namespace` is optional and silently omitted from the joined string when empty.
336
+ `key` is required and must be non-empty. `namespace` is optional - it's silently omitted from the joined string when empty.
334
337
 
335
338
  ```typescript
336
339
  container.bind({ key: 'workers.EmailWorker' }).toClass(EmailWorker).setTags('background', 'email');
@@ -356,11 +359,13 @@ class ApplicationError extends Error {
356
359
  getError(opts: TError): ApplicationError; // factory function
357
360
  ```
358
361
 
359
- `opts.message` accepts two shapes: the historical string (paired with sibling `messageCode?`/`messageArgs?`), or an object mirroring `normalized` - `{ text, code?, args? }`. Both resolve to the same `normalized`; `messageCode`/`messageArgs` are lowest precedence, so `message.code`/`message.args` (or a catalogued definition's own `message.code`/`message.args`) win when both are present. There is no flat `error.messageCode`, and `extra` never mirrors `messageArgs`. `normalized.args` is always populated (`{}` when empty).
362
+ `opts.message` accepts two shapes: the historical string (paired with sibling `messageCode?`/`messageArgs?`), or an object mirroring `normalized` - `{ text, code?, args? }`. Both resolve to the same `normalized`.
360
363
 
361
- The catalogued form (`{ error: TErrorDefinition }`) takes `message` as a **partial** override - `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`. `error` is refused on the free-form branch (`error?: never`) - wrap a caught failure with `cause` instead.
364
+ `messageCode`/`messageArgs` are lowest precedence - `message.code`/`message.args`, or a catalogued definition's own, win when both are present. There is no flat `error.messageCode`. `extra` never mirrors `messageArgs`. `normalized.args` is always populated (`{}` when empty).
362
365
 
363
- `ApplicationError`'s constructor defaults `statusCode` to `400` when omitted, and moves any property it does not model into `this.extra`. The error RESPONSE schema (`ErrorSchema`, for OpenAPI) lives in `@venizia/ignis-helpers`, not here - it needs `@hono/zod-openapi`, which inversion must not depend on because it ships to browsers.
366
+ The catalogued form (`{ error: TErrorDefinition }`) takes `message` as a **partial** override. `{ message: { args } }` amends just the args and keeps the definition's `text`/`code`. `error` is refused on the free-form branch (`error?: never`) - wrap a caught failure with `cause` instead.
367
+
368
+ `ApplicationError`'s constructor defaults `statusCode` to `400` when omitted, and moves any property it does not model into `this.extra`. The error RESPONSE schema (`ErrorSchema`, for OpenAPI) lives in `@venizia/ignis-helpers`, not here. It needs `@hono/zod-openapi`, which inversion must not depend on - inversion ships to browsers.
364
369
 
365
370
  ```typescript
366
371
  throw getError({ message: 'Something failed', statusCode: 500, messageCode: 'ERR_INTERNAL' });
@@ -407,7 +412,7 @@ interface IBindingTag {
407
412
  function isClass<T>(target: any): target is TClass<T>;
408
413
  ```
409
414
 
410
- `isClass` tests `typeof target === 'function' && target.prototype !== undefined` plus a regex match on the function's stringified source (`/^class[\s{]/`) - it relies on the class being emitted as an ES2024 `class`, not transpiled down to an ES5 constructor function.
415
+ `isClass` tests `typeof target === 'function' && target.prototype !== undefined` plus a regex match on the function's stringified source (`/^class[\s{]/`). It relies on the class being emitted as an ES2024 `class`, not transpiled down to an ES5 constructor function.
411
416
 
412
417
  ```typescript
413
418
  interface IProvider<T> {
@@ -438,7 +443,7 @@ function isClassProvider<T>(target: any): target is TClass<IProvider<T>>;
438
443
  **Fix:**
439
444
  1. Verify the binding exists: `container.isBound({ key: 'services.UserService' })`.
440
445
  2. Check for typos between `@inject({ key: '...' })` and the key used in `container.bind({ key: '...' })`.
441
- 3. If the dependency is genuinely optional, use `@inject({ key: '...', isOptional: true })` on a **constructor parameter** (not a property - see the warning above), or `container.get({ key: '...', isOptional: true })`.
446
+ 3. If the dependency is genuinely optional, add `isOptional: true` to the `@inject` call - constructor parameter or property, both work - or use `container.get({ key: '...', isOptional: true })`.
442
447
 
443
448
  ### "[getValue] Invalid context/container to instantiate class"
444
449
 
@@ -472,9 +477,12 @@ function isClassProvider<T>(target: any): target is TClass<IProvider<T>>;
472
477
 
473
478
  ### "[ClassName] Constructor parameter N has no @inject"
474
479
 
475
- **Cause:** A container-instantiated class has a constructor mixing decorated and undecorated parameters. `@inject` stores metadata at the parameter's index, so an undecorated parameter leaves a hole in that array; there is no channel through which the container could supply it anyway.
480
+ **Cause:** A container-instantiated class has a constructor mixing decorated and undecorated parameters. `@inject` stores metadata at the parameter's index, so an undecorated parameter leaves a hole in that array. There is no channel through which the container could supply it anyway.
481
+
482
+ **Fix:** Decorate every constructor parameter with `@inject`. There is no partial-injection escape hatch. If a value doesn't come from the container (e.g. a plain `scope: string`), choose one:
476
483
 
477
- **Fix:** Decorate every constructor parameter with `@inject`. There is no partial-injection escape hatch - if a value does not come from the container (e.g. a plain `scope: string`), pass it through a factory/provider instead of a bare constructor parameter, or have the subclass forward it via its own `@inject`-decorated parameter.
484
+ - Pass it through a factory/provider instead of a bare constructor parameter.
485
+ - Have the subclass forward it via its own `@inject`-decorated parameter.
478
486
 
479
487
  ### "@inject decorator can only be used on class properties or constructor parameters"
480
488
 
@@ -482,11 +490,11 @@ function isClassProvider<T>(target: any): target is TClass<IProvider<T>>;
482
490
 
483
491
  **Fix:** Only use `@inject` on constructor parameters or class properties.
484
492
 
485
- ### Property injection returns `undefined` even without `isOptional`
493
+ ### Property injection never runs
486
494
 
487
- **Cause:** Either (a) the class was instantiated with `new MyClass()` directly instead of through the container - only the container reads `@inject` metadata and populates properties - or (b) the binding key really is unbound, and the `isOptional: true` you set on the property has no effect (see the phase-2 warning above, `metadata.optional` vs `metadata.isOptional`).
495
+ **Cause:** The class was instantiated with `new MyClass()` directly instead of through the container. Only `container.resolve()`/`instantiate()` reads `@inject` metadata and populates properties. A plain `new` leaves them at whatever their field initializer set - `undefined` if none.
488
496
 
489
- **Fix:** Always use `container.resolve(MyClass)` or `container.instantiate(MyClass)` to create instances. If the dependency may legitimately be absent, inject it on the constructor instead of a property, or resolve it manually inside the constructor with `container.get({ key, isOptional: true })`.
497
+ **Fix:** Always use `container.resolve(MyClass)` or `container.instantiate(MyClass)` to create instances that use property injection.
490
498
 
491
499
  ### "getInjectMetadata returns undefined"
492
500
 
@@ -13,7 +13,7 @@ class KafkaAdminHelper extends BaseKafkaHelper<Admin>
13
13
  ```
14
14
 
15
15
  > [!NOTE]
16
- > `KafkaAdminHelper` has **no generic type parameters** -- the Admin client does not deal with serialized messages.
16
+ > `KafkaAdminHelper` has **no generic type parameters** - the Admin client does not deal with serialized messages.
17
17
 
18
18
  ## Helper API
19
19
 
@@ -24,6 +24,7 @@ class KafkaAdminHelper extends BaseKafkaHelper<Admin>
24
24
  | `isHealthy()` | `(): boolean` | `true` when broker connected |
25
25
  | `isReady()` | `(): boolean` | Same as `isHealthy()` |
26
26
  | `getHealthStatus()` | `(): TKafkaHealthStatus` | `'connected'` \| `'disconnected'` \| `'unknown'` |
27
+ | `getConnectedBrokerCount()` | `(): number` | Number of currently connected brokers |
27
28
  | `close(opts?)` | `(opts?: { isForce?: boolean }): Promise<void>` | Close the admin connection (default: graceful) |
28
29
 
29
30
  ## IKafkaAdminOptions
@@ -71,7 +72,7 @@ await helper.close({ isForce: true });
71
72
 
72
73
  ## Graceful Shutdown
73
74
 
74
- `close()` uses the base `closeClient()` (which calls `this.client.close()`) with a graceful timeout. If the graceful close exceeds `shutdownTimeout` (default 30s), it automatically force-closes. After `close()`, `healthStatus` is set to `'disconnected'`.
75
+ `close()` uses the base `closeClient()`, wrapped in a graceful timeout. `closeClient()` calls `this.client.close()` directly. If the graceful close exceeds `shutdownTimeout` (default 30s), it automatically force-closes. After `close()`, `healthStatus` becomes `'disconnected'`.
75
76
 
76
77
  ```typescript
77
78
  // Graceful (recommended)