@venizia/ignis-inversion 0.1.1-0 → 0.1.1-2

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 (82) hide show
  1. package/README.md +117 -2102
  2. package/dist/cjs/common/types.d.ts +2 -11
  3. package/dist/cjs/common/types.d.ts.map +1 -1
  4. package/dist/cjs/common/types.js +2 -11
  5. package/dist/cjs/common/types.js.map +1 -1
  6. package/dist/cjs/modules/container/abstract.d.ts +2 -4
  7. package/dist/cjs/modules/container/abstract.d.ts.map +1 -1
  8. package/dist/cjs/modules/container/abstract.js +2 -4
  9. package/dist/cjs/modules/container/abstract.js.map +1 -1
  10. package/dist/cjs/modules/container/base.d.ts +2 -4
  11. package/dist/cjs/modules/container/base.d.ts.map +1 -1
  12. package/dist/cjs/modules/container/base.js +2 -4
  13. package/dist/cjs/modules/container/base.js.map +1 -1
  14. package/dist/cjs/modules/container/container.js +1 -1
  15. package/dist/cjs/modules/container/container.js.map +1 -1
  16. package/dist/cjs/modules/error/app-error.d.ts +0 -1
  17. package/dist/cjs/modules/error/app-error.d.ts.map +1 -1
  18. package/dist/cjs/modules/error/app-error.js +14 -20
  19. package/dist/cjs/modules/error/app-error.js.map +1 -1
  20. package/dist/cjs/modules/error/definition.d.ts +0 -33
  21. package/dist/cjs/modules/error/definition.d.ts.map +1 -1
  22. package/dist/cjs/modules/error/message-code.d.ts +8 -4
  23. package/dist/cjs/modules/error/message-code.d.ts.map +1 -1
  24. package/dist/cjs/modules/error/message-code.js +11 -3
  25. package/dist/cjs/modules/error/message-code.js.map +1 -1
  26. package/dist/cjs/modules/error/types.d.ts +53 -12
  27. package/dist/cjs/modules/error/types.d.ts.map +1 -1
  28. package/dist/cjs/modules/metadata/common/constants.d.ts +0 -1
  29. package/dist/cjs/modules/metadata/common/constants.d.ts.map +1 -1
  30. package/dist/cjs/modules/metadata/common/constants.js +0 -1
  31. package/dist/cjs/modules/metadata/common/constants.js.map +1 -1
  32. package/dist/cjs/modules/metadata/injectors.d.ts +0 -2
  33. package/dist/cjs/modules/metadata/injectors.d.ts.map +1 -1
  34. package/dist/cjs/modules/metadata/injectors.js +1 -7
  35. package/dist/cjs/modules/metadata/injectors.js.map +1 -1
  36. package/dist/cjs/modules/registry/common/types.d.ts +0 -6
  37. package/dist/cjs/modules/registry/common/types.d.ts.map +1 -1
  38. package/dist/cjs/modules/registry/registry.d.ts +1 -8
  39. package/dist/cjs/modules/registry/registry.d.ts.map +1 -1
  40. package/dist/cjs/modules/registry/registry.js +0 -8
  41. package/dist/cjs/modules/registry/registry.js.map +1 -1
  42. package/dist/esm/common/types.d.ts +2 -11
  43. package/dist/esm/common/types.d.ts.map +1 -1
  44. package/dist/esm/common/types.js +2 -11
  45. package/dist/esm/common/types.js.map +1 -1
  46. package/dist/esm/modules/container/abstract.d.ts +2 -4
  47. package/dist/esm/modules/container/abstract.d.ts.map +1 -1
  48. package/dist/esm/modules/container/abstract.js +2 -4
  49. package/dist/esm/modules/container/abstract.js.map +1 -1
  50. package/dist/esm/modules/container/base.d.ts +2 -4
  51. package/dist/esm/modules/container/base.d.ts.map +1 -1
  52. package/dist/esm/modules/container/base.js +2 -4
  53. package/dist/esm/modules/container/base.js.map +1 -1
  54. package/dist/esm/modules/container/container.js +1 -1
  55. package/dist/esm/modules/container/container.js.map +1 -1
  56. package/dist/esm/modules/error/app-error.d.ts +0 -1
  57. package/dist/esm/modules/error/app-error.d.ts.map +1 -1
  58. package/dist/esm/modules/error/app-error.js +14 -20
  59. package/dist/esm/modules/error/app-error.js.map +1 -1
  60. package/dist/esm/modules/error/definition.d.ts +0 -33
  61. package/dist/esm/modules/error/definition.d.ts.map +1 -1
  62. package/dist/esm/modules/error/message-code.d.ts +8 -4
  63. package/dist/esm/modules/error/message-code.d.ts.map +1 -1
  64. package/dist/esm/modules/error/message-code.js +11 -3
  65. package/dist/esm/modules/error/message-code.js.map +1 -1
  66. package/dist/esm/modules/error/types.d.ts +53 -12
  67. package/dist/esm/modules/error/types.d.ts.map +1 -1
  68. package/dist/esm/modules/metadata/common/constants.d.ts +0 -1
  69. package/dist/esm/modules/metadata/common/constants.d.ts.map +1 -1
  70. package/dist/esm/modules/metadata/common/constants.js +0 -1
  71. package/dist/esm/modules/metadata/common/constants.js.map +1 -1
  72. package/dist/esm/modules/metadata/injectors.d.ts +0 -2
  73. package/dist/esm/modules/metadata/injectors.d.ts.map +1 -1
  74. package/dist/esm/modules/metadata/injectors.js +0 -5
  75. package/dist/esm/modules/metadata/injectors.js.map +1 -1
  76. package/dist/esm/modules/registry/common/types.d.ts +0 -6
  77. package/dist/esm/modules/registry/common/types.d.ts.map +1 -1
  78. package/dist/esm/modules/registry/registry.d.ts +1 -8
  79. package/dist/esm/modules/registry/registry.d.ts.map +1 -1
  80. package/dist/esm/modules/registry/registry.js +0 -8
  81. package/dist/esm/modules/registry/registry.js.map +1 -1
  82. package/package.json +3 -3
package/README.md CHANGED
@@ -1,2169 +1,184 @@
1
1
  <div align="center">
2
2
 
3
- # :fire: IGNIS - @venizia/ignis-inversion
3
+ <br />
4
4
 
5
- **Standalone, lightweight DI/IoC container for TypeScript**
5
+ # :fire: @venizia/ignis-inversion
6
6
 
7
- [![npm](https://img.shields.io/npm/v/@venizia/ignis-inversion.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@venizia/ignis-inversion)
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
+ **A small IoC container that works anywhere.**
10
8
 
11
- A ~350-line Dependency Injection & Inversion of Control (IoC) container featuring decorator-based constructor and property injection, fluent Binding API, singleton/transient scoping, namespace auto-tagging, Provider pattern, and tag-based discovery. Part of the [IGNIS Framework](https://github.com/VENIZIA-AI/ignis).
9
+ [![Docs](https://img.shields.io/badge/Docs-ignis.venizia.ai-2563EB.svg?style=flat-square)](https://ignis.venizia.ai/references/base/dependency-injection)
10
+ [![npm](https://img.shields.io/npm/v/@venizia/ignis-inversion.svg?style=flat-square&color=cb3837&label=@venizia/ignis-inversion)](https://www.npmjs.com/package/@venizia/ignis-inversion)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](https://opensource.org/licenses/MIT)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
12
13
 
13
- [Installation](#installation) &#8226; [Quick Start](#quick-start) &#8226; [API Reference](#container) &#8226; [Documentation](https://venizia-ai.github.io/ignis)
14
+ [Documentation](https://ignis.venizia.ai/references/base/dependency-injection) &#8226;
15
+ [Core API](https://ignis.venizia.ai/references/) &#8226;
16
+ [Changelog](https://ignis.venizia.ai/changelogs/)
14
17
 
15
18
  </div>
16
19
 
17
- ## Highlights
18
-
19
- | | Feature | |
20
- | :---: | :--- | :--- |
21
- | **1** | **~350 Lines of Core** | Full IoC container with zero bloat |
22
- | **2** | **Decorator-Based DI** | `@inject` for constructor and property injection |
23
- | **3** | **Fluent Binding API** | Chain `.toClass()`, `.toValue()`, `.toProvider()`, `.setScope()` |
24
- | **4** | **Namespace Auto-Tagging** | `"services.UserService"` auto-tags with `"services"` |
25
- | **5** | **Zero Framework Lock-in** | Works with any TypeScript project |
26
-
27
- ---
28
-
29
- ## Philosophy
30
-
31
- IGNIS Inversion takes the best ideas from **LoopBack 4**'s IoC system -- decorator-based injection, fluent binding configuration, namespace-driven organization -- and strips them down to ~350 lines of focused, zero-overhead container logic. No complex module system, no provider hierarchies, no framework lock-in. Just a fast, type-safe container that works with any TypeScript project.
32
-
33
- **Why this exists:**
34
-
35
- | Library | Limitation |
36
- |---------|------------|
37
- | **LoopBack 4** | Had the right DI architecture but came bundled with an entire framework (now effectively abandoned) |
38
- | **NestJS** | Modules add ceremony and indirection when all you need is straightforward DI |
39
- | **InversifyJS** | Powerful but heavy for projects that need a simple, fast container |
40
- | **tsyringe** | Minimal but lacks fluent configuration, tagging, and property injection |
41
-
42
- IGNIS Inversion gives you **constructor injection, property injection, singleton/transient scoping, tag-based discovery, and a provider pattern** -- all in a single dependency with no runtime overhead.
43
-
44
20
  ---
45
21
 
46
- ## Table of Contents
47
-
48
- - [Installation](#installation)
49
- - [Quick Start](#quick-start)
50
- - [Core Concepts](#core-concepts)
51
- - [Container](#container)
52
- - [Binding](#binding)
53
- - [MetadataRegistry](#metadataregistry)
54
- - [Decorators](#decorators)
55
- - [Binding Keys](#binding-keys)
56
- - [BaseHelper](#basehelper)
57
- - [Advanced Usage](#advanced-usage)
58
- - [Constructor Injection](#constructor-injection)
59
- - [Property Injection](#property-injection)
60
- - [Provider Pattern](#provider-pattern)
61
- - [Optional Dependencies](#optional-dependencies)
62
- - [Singleton vs Transient Scoping](#singleton-vs-transient-scoping)
63
- - [Tag-Based Discovery](#tag-based-discovery)
64
- - [Type Guards and Utilities](#type-guards-and-utilities)
65
- - [Container Lifecycle](#container-lifecycle)
66
- - [Create, Bind, Resolve, Teardown](#create-bind-resolve-teardown)
67
- - [clear() vs reset()](#clear-vs-reset)
68
- - [Binding Resolution Internals](#binding-resolution-internals)
69
- - [Two-Phase Instantiation Walkthrough](#two-phase-instantiation-walkthrough)
70
- - [Resolver Types and getValue() Flow](#resolver-types-and-getvalue-flow)
71
- - [Singleton Caching Internals](#singleton-caching-internals)
72
- - [Provider Pattern Deep Dive](#provider-pattern-deep-dive)
73
- - [Function Providers](#function-providers)
74
- - [Class-Based Providers (IProvider)](#class-based-providers-iprovider)
75
- - [When to Use Each](#when-to-use-each)
76
- - [Scope Behavior Details](#scope-behavior-details)
77
- - [Transient Behavior](#transient-behavior)
78
- - [Singleton Behavior](#singleton-behavior)
79
- - [Cache Management](#cache-management)
80
- - [Real-World Example: Mini Application](#real-world-example-mini-application)
81
- - [Container Independence](#container-independence)
82
- - [Integration with Other IGNIS Packages](#integration-with-other-ignis-packages)
83
- - [How Core Uses Inversion](#how-core-uses-inversion)
84
- - [How Boot Uses Inversion](#how-boot-uses-inversion)
85
- - [MetadataRegistry Extension via Mixins](#metadataregistry-extension-via-mixins)
86
- - [Symbol.for Metadata Keys](#symbolforkk-metadata-keys)
87
- - [Performance Considerations](#performance-considerations)
88
- - [Types](#types)
89
- - [Core Types](#core-types)
90
- - [DI-Specific Types](#di-specific-types)
91
- - [Metadata Types](#metadata-types)
92
- - [Metadata Keys](#metadata-keys)
93
- - [Complete Type Reference](#complete-type-reference)
94
- - [Error Handling](#error-handling)
95
- - [Common Error Scenarios](#common-error-scenarios)
96
- - [Error Catalog](#error-catalog)
97
- - [API Reference](#api-reference)
98
- - [Container API](#container-api)
99
- - [Binding API](#binding-api)
100
- - [MetadataRegistry API](#metadataregistry-api)
101
- - [Decorators API](#decorators-api)
102
- - [Utility Functions](#utility-functions)
103
- - [Migration Guide](#migration-guide)
104
- - [From InversifyJS](#from-inversifyjs)
105
- - [From tsyringe](#from-tsyringe)
106
- - [Troubleshooting](#troubleshooting)
107
- - [License](#license)
22
+ The dependency injection container behind [IGNIS](https://ignis.venizia.ai) - decorator-driven
23
+ constructor and property injection, a fluent binding API, singleton/transient scopes, and tag-based
24
+ discovery, in roughly 350 lines.
108
25
 
109
- ---
26
+ It has no framework dependency. Reach for it when you want LoopBack 4-style DI without adopting a
27
+ framework, and it is small enough to ship into a browser bundle. IGNIS itself uses nothing more than
28
+ what is documented here.
110
29
 
111
- ## Installation
30
+ ## Install
112
31
 
113
32
  ```bash
114
- bun add @venizia/ignis-inversion
115
- # or
116
- npm install @venizia/ignis-inversion
117
- ```
118
-
119
- **Requirements:**
120
-
121
- - TypeScript 5.x with `experimentalDecorators` and `emitDecoratorMetadata` enabled in `tsconfig.json`:
122
-
123
- ```json
124
- {
125
- "compilerOptions": {
126
- "experimentalDecorators": true,
127
- "emitDecoratorMetadata": true
128
- }
129
- }
33
+ bun add @venizia/ignis-inversion reflect-metadata
130
34
  ```
131
35
 
132
- **Peer dependency:** `reflect-metadata` is included as a direct dependency and automatically imported.
133
-
134
- ---
135
-
136
- ## Quick Start
137
-
138
- ### Basic Container Usage
139
-
140
- ```typescript
141
- import { Container } from '@venizia/ignis-inversion';
142
-
143
- const container = new Container();
144
-
145
- // Bind a plain value
146
- container.bind({ key: 'config.dbHost' }).toValue('localhost');
147
-
148
- // Bind a class
149
- class Logger {
150
- log(msg: string) {
151
- console.log(msg);
152
- }
153
- }
154
-
155
- container.bind({ key: 'Logger' }).toClass(Logger);
156
-
157
- // Resolve dependencies
158
- const dbHost = container.get<string>({ key: 'config.dbHost' });
159
- const logger = container.get<Logger>({ key: 'Logger' });
160
-
161
- logger.log(`Connecting to ${dbHost}`);
162
- ```
36
+ > [!IMPORTANT]
37
+ > `experimentalDecorators` and `emitDecoratorMetadata` must be `true` in your `tsconfig.json`,
38
+ > declared **inline**. Bun does not resolve them through `extends`, and `@inject` is silently
39
+ > dropped without them.
163
40
 
164
- ### Decorator-Based Dependency Injection
41
+ ## Usage
165
42
 
166
43
  ```typescript
167
- import { Container, injectable, inject } from '@venizia/ignis-inversion';
44
+ import 'reflect-metadata';
45
+ import { BindingScopes, Container, inject } from '@venizia/ignis-inversion';
168
46
 
169
- @injectable({})
170
- class UserRepository {
171
- findById(id: string) {
172
- return { id, name: 'Alice' };
173
- }
174
- }
175
-
176
- @injectable({})
177
- class UserService {
47
+ class GreetingService {
178
48
  constructor(
179
- @inject({ key: 'repositories.UserRepository' })
180
- private userRepo: UserRepository,
49
+ @inject({ key: 'config.prefix' }) private readonly prefix: string,
50
+ @inject({ key: 'config.locale', isOptional: true }) private readonly locale?: string,
181
51
  ) {}
182
52
 
183
- getUser(id: string) {
184
- return this.userRepo.findById(id);
53
+ greet(name: string) {
54
+ return `${this.prefix} ${name} (${this.locale ?? 'en'})`;
185
55
  }
186
56
  }
187
57
 
188
- // Wire it up
189
- const container = new Container();
58
+ const container = new Container({ scope: 'app' });
59
+
60
+ container.bind({ key: 'config.prefix' }).toValue('Hello');
190
61
  container
191
- .bind({ key: 'repositories.UserRepository' })
192
- .toClass(UserRepository)
193
- .setScope('singleton');
62
+ .bind({ key: 'services.GreetingService' })
63
+ .toClass(GreetingService)
64
+ .setScope(BindingScopes.SINGLETON);
194
65
 
195
- container.bind({ key: 'services.UserService' }).toClass(UserService);
66
+ const service = container.get<GreetingService>({ key: 'services.GreetingService' });
67
+ console.log(service.greet('IGNIS')); // Hello IGNIS (en)
196
68
 
197
- // Resolve -- constructor dependencies are injected automatically
198
- const service = container.get<UserService>({ key: 'services.UserService' });
199
- service.getUser('123'); // { id: '123', name: 'Alice' }
69
+ // A key's first segment becomes a tag, so bindings are discoverable as a group.
70
+ container.findByTag({ tag: 'services' }); // [Binding('services.GreetingService')]
200
71
  ```
201
72
 
202
- ---
73
+ ## API
203
74
 
204
- ## Core Concepts
75
+ Every method takes an options object - `container.get({ key })`, never `container.get(key)`.
205
76
 
206
77
  ### Container
207
78
 
208
- The `Container` is the central IoC registry. It stores bindings, resolves dependencies, and performs two-phase instantiation (constructor injection, then property injection).
209
-
210
- ```typescript
211
- import { Container } from '@venizia/ignis-inversion';
212
-
213
- // Create a container (optional scope name for debugging)
214
- const container = new Container({ scope: 'MyApp' });
215
- ```
216
-
217
- **Key operations:**
218
-
219
- ```typescript
220
- // Register a binding
221
- container.bind<MyService>({ key: 'services.MyService' }).toClass(MyService);
222
-
223
- // Resolve a dependency
224
- const svc = container.get<MyService>({ key: 'services.MyService' });
225
-
226
- // Check if a binding exists
227
- container.isBound({ key: 'services.MyService' }); // true
228
-
229
- // Remove a binding
230
- container.unbind({ key: 'services.MyService' }); // true
231
-
232
- // Resolve multiple bindings at once
233
- const [db, cache] = container.gets<[Database, Cache]>({
234
- bindings: [
235
- { key: 'datasources.Database' },
236
- { key: 'datasources.Cache', isOptional: true },
237
- ],
238
- });
239
-
240
- // Instantiate a class with DI without registering it
241
- const instance = container.resolve<MyService>(MyService);
242
- // or equivalently:
243
- const instance2 = container.instantiate<MyService>(MyService);
244
-
245
- // Clear all cached singleton values (bindings remain)
246
- container.clear();
247
-
248
- // Remove all bindings entirely
249
- container.reset();
250
-
251
- // Access the global metadata registry
252
- const registry = container.getMetadataRegistry();
253
- ```
254
-
255
- **Namespaced key lookup:**
256
-
257
- The `get` and `getBinding` methods accept keys in three formats:
258
-
259
- ```typescript
260
- // String key
261
- container.get<MyService>({ key: 'services.MyService' });
262
-
263
- // Symbol key
264
- container.get<MyService>({ key: Symbol.for('services.MyService') });
265
-
266
- // Structured namespace + key object (built via BindingKeys.build)
267
- container.get<MyService>({ key: { namespace: 'services', key: 'MyService' } });
268
- ```
269
-
270
- **Two-phase instantiation algorithm:**
271
-
272
- When a class binding is resolved, the container:
273
-
274
- 1. **Constructor injection** -- Reads `@inject` metadata from the class, sorts parameters by index, resolves each dependency from the container, and calls `new Class(...resolvedArgs)`.
275
- 2. **Property injection** -- Reads property `@inject` metadata from the instance, resolves each dependency, and assigns it to the corresponding property.
276
-
277
- ---
79
+ | Member | What it does |
80
+ | :--- | :--- |
81
+ | `new Container({ scope })` | The default container. `AbstractContainer` (contract) and `BaseContainer` (storage) are exported for custom implementations |
82
+ | `bind<T>({ key })` | Creates and registers a `Binding`, returned for chaining |
83
+ | `get<T>({ key, isOptional })` | Resolves a value. Throws when unbound unless `isOptional` is true |
84
+ | `gets<T>({ bindings })` | Resolves several keys at once; every result is optional |
85
+ | `getBinding<T>({ key })` | The binding itself, not its value. Accepts `{ namespace, key }` too |
86
+ | `isBound({ key })` / `unbind({ key })` | Membership test / removal |
87
+ | `set({ binding })` | Registers an externally constructed `Binding` under its own key |
88
+ | `instantiate<T>(cls)` | Builds a class from its decorator metadata - `resolve(cls)` is an alias |
89
+ | `findByTag({ tag, exclude })` | All bindings carrying a tag. `exclude` takes an array or a `Set` of keys |
90
+ | `clear()` / `reset()` | Drops cached singleton instances / drops all bindings |
278
91
 
279
92
  ### Binding
280
93
 
281
- A `Binding<T>` represents a registered dependency and how to resolve it. Bindings are created via `container.bind()` and configured with a fluent API.
282
-
283
- ```typescript
284
- const binding = container.bind<UserService>({ key: 'services.UserService' });
285
- ```
286
-
287
- **Binding value types:**
288
-
289
- ```typescript
290
- // Class -- container instantiates with DI
291
- binding.toClass(UserService);
292
-
293
- // Value -- returns the exact value
294
- binding.toValue({ host: 'localhost', port: 5432 });
295
-
296
- // Provider -- factory function or IProvider class
297
- binding.toProvider((container) => {
298
- const config = container.get<Config>({ key: 'config' });
299
- return new DatabasePool(config);
300
- });
301
- ```
302
-
303
- **Fluent configuration:**
304
-
305
- ```typescript
306
- container
307
- .bind<UserService>({ key: 'services.UserService' })
308
- .toClass(UserService)
309
- .setScope('singleton')
310
- .setTags('critical', 'user-domain');
311
- ```
312
-
313
- **Direct binding insertion:**
314
-
315
- You can also create a binding externally and insert it into the container:
316
-
317
- ```typescript
318
- import { Binding } from '@venizia/ignis-inversion';
319
-
320
- const binding = Binding.bind<Config>({ key: 'config' });
321
- binding.toValue({ port: 3000 });
322
-
323
- container.set({ binding });
324
- ```
325
-
326
- ---
327
-
328
- ### MetadataRegistry
329
-
330
- The `MetadataRegistry` is a singleton that stores all decorator metadata using `reflect-metadata`. It is the bridge between decorators (compile-time annotations) and the container (runtime resolution).
331
-
332
- ```typescript
333
- import { metadataRegistry } from '@venizia/ignis-inversion';
334
-
335
- // Or access it via a container instance
336
- const registry = container.getMetadataRegistry();
337
- ```
338
-
339
- **Generic metadata storage:**
340
-
341
- ```typescript
342
- // Store arbitrary metadata on any target
343
- registry.define({ target: MyClass, key: 'custom:role', value: 'admin' });
344
-
345
- // Retrieve it
346
- const role = registry.get<typeof MyClass, string>({
347
- target: MyClass,
348
- key: 'custom:role',
349
- });
350
-
351
- // Check existence
352
- registry.has({ target: MyClass, key: 'custom:role' }); // true
353
-
354
- // Remove
355
- registry.delete({ target: MyClass, key: 'custom:role' }); // true
356
- ```
357
-
358
- **Introspection:**
359
-
360
- ```typescript
361
- // Get all metadata keys for a target
362
- const keys = registry.getKeys({ target: MyClass });
363
-
364
- // Get all non-constructor method names from a class
365
- const methods = registry.getMethodNames({ target: MyClass });
366
-
367
- // Clear all metadata for a target
368
- registry.clearMetadata({ target: MyClass });
369
- ```
370
-
371
- The registry also provides specialized methods for injection metadata (`setInjectMetadata`, `getInjectMetadata`), property metadata (`setPropertyMetadata`, `getPropertiesMetadata`, `getPropertyMetadata`), and injectable metadata (`setInjectableMetadata`, `getInjectableMetadata`). These are typically used internally by the `@inject` and `@injectable` decorators.
372
-
373
- **How injection metadata is stored:**
374
-
375
- - **Constructor `@inject`:** Stored on the **class constructor** itself using `MetadataKeys.INJECT` as the reflect-metadata key. The value is an array of `IInjectMetadata` objects, indexed by parameter position.
376
- - **Property `@inject`:** Stored on the **class constructor** (via `target.constructor`) using `MetadataKeys.PROPERTIES` as the reflect-metadata key. The value is a `Map<string | symbol, IPropertyMetadata>` mapping property names to their injection config.
377
- - **`@injectable`:** Stored on the **class constructor** using `MetadataKeys.INJECTABLE`. The value is an `IInjectableMetadata` object with optional scope and tags.
378
-
379
- ---
94
+ | Member | What it does |
95
+ | :--- | :--- |
96
+ | `.toValue(value)` | Binds a ready value - returned as-is |
97
+ | `.toClass(Class)` | Binds a class, instantiated through the container on resolution |
98
+ | `.toProvider(fn \| ProviderClass)` | Binds a factory `(container) => T`, or a class implementing `IProvider<T>` with a `value(container)` method |
99
+ | `.setScope(scope)` / `.getScope()` | `BindingScopes.TRANSIENT` (default) or `BindingScopes.SINGLETON` |
100
+ | `.setTags(...tags)` / `.hasTag(tag)` / `.getTags()` | Tagging for `findByTag` |
101
+ | `.getValue(container)` | Resolves the binding; a container is required for class and provider resolvers |
102
+ | `.clearCache()` | Discards a cached singleton instance |
380
103
 
381
104
  ### Decorators
382
105
 
383
- Two decorators form the DI annotation layer:
384
-
385
- #### `@injectable(metadata)`
386
-
387
- Marks a class as injectable with optional scope and tag metadata.
388
-
389
- ```typescript
390
- import { injectable } from '@venizia/ignis-inversion';
391
-
392
- @injectable({ scope: 'singleton', tags: { domain: 'user' } })
393
- class UserService {
394
- // ...
395
- }
396
- ```
397
-
398
- | Parameter | Type | Description |
399
- |:----------|:-----|:------------|
400
- | `metadata.scope` | `'singleton' \| 'transient'` | Optional. Default binding scope hint |
401
- | `metadata.tags` | `Record<string, any>` | Optional. Arbitrary tag metadata |
402
-
403
- #### `@inject(opts)`
404
-
405
- Marks a constructor parameter or class property for dependency injection.
406
-
407
- ```typescript
408
- import { inject } from '@venizia/ignis-inversion';
409
-
410
- class OrderService {
411
- // Property injection
412
- @inject({ key: 'services.NotificationService' })
413
- private notifier!: NotificationService;
106
+ | Decorator | Where | Notes |
107
+ | :--- | :--- | :--- |
108
+ | `@inject({ key, isOptional? })` | Constructor parameter | Resolved by index and passed to the constructor |
109
+ | `@inject({ key, isOptional? })` | Property | Assigned after construction |
414
110
 
415
- constructor(
416
- // Constructor parameter injection
417
- @inject({ key: 'repositories.OrderRepository' })
418
- private orderRepo: OrderRepository,
419
-
420
- // Optional dependency -- resolves to undefined if not bound
421
- @inject({ key: 'services.AuditService', isOptional: true })
422
- private auditService?: AuditService,
423
- ) {}
424
- }
425
- ```
426
-
427
- | Parameter | Type | Description |
428
- |:----------|:-----|:------------|
429
- | `opts.key` | `string \| symbol` | The binding key to resolve |
430
- | `opts.isOptional` | `boolean` | If `true`, returns `undefined` instead of throwing when unbound. Default: `false` |
431
- | `opts.registry` | `MetadataRegistry` | Optional. Override the default global registry |
432
-
433
- **What happens internally when `@inject` is applied:**
434
-
435
- - If applied to a **constructor parameter** (when `parameterIndex` is a number), it calls `registry.setInjectMetadata()` which stores `{ key, index, isOptional }` in a reflect-metadata array on the class constructor.
436
- - If applied to a **property** (when `propertyName` is defined), it calls `registry.setPropertyMetadata()` which stores `{ bindingKey, isOptional }` in a reflect-metadata Map on the class constructor.
437
- - If applied in any other context, it throws an `ApplicationError` with the message `@inject decorator can only be used on class properties or constructor parameters`.
438
-
439
- ---
440
-
441
- ### Binding Keys
442
-
443
- IGNIS uses a **namespace-based naming convention** for binding keys. This keeps large applications organized and enables tag-based discovery.
444
-
445
- ```typescript
446
- import { BindingKeys } from '@venizia/ignis-inversion';
447
-
448
- // Build a namespaced key
449
- const key = BindingKeys.build({ namespace: 'services', key: 'UserService' });
450
- // => 'services.UserService'
451
- ```
452
-
453
- **Convention:** `namespace.ClassName`
454
-
455
- | Namespace | Usage |
456
- |:----------|:------|
457
- | `controllers` | HTTP controllers |
458
- | `services` | Business logic services |
459
- | `repositories` | Data access repositories |
460
- | `datasources` | Database connections |
461
- | `config` | Configuration values |
462
-
463
- **Auto-tagging:** When a binding key contains a dot (e.g., `"services.UserService"`), the text before the first dot is automatically added as a tag. This means `container.bind({ key: 'services.UserService' })` automatically tags the binding with `"services"`.
464
-
465
- ```typescript
466
- container.bind({ key: 'services.UserService' }).toClass(UserService);
467
- container.bind({ key: 'services.OrderService' }).toClass(OrderService);
468
-
469
- // Find all service bindings via the auto-tag
470
- const serviceBindings = container.findByTag({ tag: 'services' });
471
- // => [Binding<UserService>, Binding<OrderService>]
472
- ```
473
-
474
- **BindingKeys.build validation:** The `key` parameter is required. If it is empty, `BindingKeys.build` throws an `ApplicationError`. The `namespace` parameter is optional -- if empty, the result is just the key without a dot prefix.
475
-
476
- ```typescript
477
- BindingKeys.build({ namespace: '', key: 'AppConfig' }); // => 'AppConfig'
478
- BindingKeys.build({ namespace: 'config', key: '' }); // throws ApplicationError
479
- ```
480
-
481
- ---
482
-
483
- ### BaseHelper
484
-
485
- Both `Container` and `Binding` extend `BaseHelper`, a minimal base class that provides debugging context:
486
-
487
- ```typescript
488
- export class BaseHelper {
489
- scope: string; // Descriptive scope name (e.g., 'MyApp' for Container, 'services.UserService' for Binding)
490
- identifier: string; // Optional secondary identifier
491
-
492
- constructor(opts: { scope: string; identifier?: string });
493
- }
494
- ```
495
-
496
- - **Container** sets `scope` to the constructor's `opts.scope` or `'Container'` by default.
497
- - **Binding** sets `scope` to its binding key string (e.g., `'services.UserService'`).
498
- - **MetadataRegistry** sets `scope` to `'MetadataRegistry'`.
499
-
500
- `BaseHelper` exists so that every DI primitive carries a human-readable label for logging and debugging. In the full IGNIS framework, the core package extends `BaseHelper` with a `Logger` instance, but the inversion package keeps it minimal.
501
-
502
- ---
503
-
504
- ## Advanced Usage
505
-
506
- ### Constructor Injection
507
-
508
- Constructor injection is the primary injection mechanism. The container reads `@inject` metadata, sorts by parameter index, resolves each dependency, and passes them as constructor arguments.
509
-
510
- ```typescript
511
- @injectable({})
512
- class PaymentService {
513
- constructor(
514
- @inject({ key: 'repositories.PaymentRepository' })
515
- private paymentRepo: PaymentRepository,
516
-
517
- @inject({ key: 'services.NotificationService' })
518
- private notifier: NotificationService,
519
-
520
- @inject({ key: 'config.stripe.apiKey' })
521
- private stripeApiKey: string,
522
- ) {}
111
+ Both write into the shared `metadataRegistry` (a `MetadataRegistry` instance). Pass your own through
112
+ the `registry` option to isolate metadata - in tests, for example.
523
113
 
524
- async charge(amount: number) {
525
- // All three dependencies are available
526
- const result = await this.paymentRepo.create({ data: { amount } });
527
- this.notifier.send(`Payment of ${amount} processed`);
528
- return result;
529
- }
530
- }
114
+ ### Keys and scopes
531
115
 
532
- // Registration
533
- container.bind({ key: 'config.stripe.apiKey' }).toValue('sk_test_...');
534
- container.bind({ key: 'repositories.PaymentRepository' }).toClass(PaymentRepository);
535
- container.bind({ key: 'services.NotificationService' }).toClass(NotificationService);
536
- container.bind({ key: 'services.PaymentService' }).toClass(PaymentService);
116
+ | Symbol | Notes |
117
+ | :--- | :--- |
118
+ | `BindingKeys.build({ namespace, key })` | Joins the two with a dot. `key` is required; an empty namespace yields the bare key |
119
+ | Namespace auto-tagging | A dotted key tags its binding with the first segment - `services.UserService` is tagged `services` |
120
+ | `BindingScopes` | `SINGLETON`, `TRANSIENT` |
121
+ | `BindingValueTypes` | `CLASS`, `VALUE`, `PROVIDER` - the resolver kinds |
537
122
 
538
- // Resolution -- all deps injected automatically
539
- const paymentService = container.get<PaymentService>({ key: 'services.PaymentService' });
540
- ```
123
+ Conventional namespaces across IGNIS: `controllers`, `services`, `repositories`, `datasources`.
541
124
 
542
- ---
125
+ Full reference: [Dependency injection](https://ignis.venizia.ai/references/base/dependency-injection).
543
126
 
544
- ### Property Injection
127
+ ## Errors
545
128
 
546
- Property injection assigns dependencies after the constructor completes. Useful for optional dependencies or to avoid long constructor parameter lists.
129
+ This package also ships the error module the whole framework throws through, so a standalone
130
+ consumer gets it for free. Never throw a raw `new Error`.
547
131
 
548
132
  ```typescript
549
- @injectable({})
550
- class ReportService {
551
- @inject({ key: 'services.Logger' })
552
- private logger!: Logger;
553
-
554
- @inject({ key: 'services.CacheService', isOptional: true })
555
- private cache?: CacheService;
556
-
557
- generate() {
558
- this.logger.log('Generating report...');
559
- if (this.cache) {
560
- // Use cache if available
561
- }
562
- }
563
- }
564
- ```
565
-
566
- > **Note:** Use the definite assignment assertion (`!`) for required property injections since TypeScript cannot see that the container will assign the value.
567
-
568
- ---
569
-
570
- ### Provider Pattern
571
-
572
- Providers allow deferred or dynamic resolution. Two forms are supported:
573
-
574
- **Function provider:**
133
+ import { getError, isApplicationError } from '@venizia/ignis-inversion';
575
134
 
576
- ```typescript
577
- container.bind({ key: 'datasources.Pool' }).toProvider((container) => {
578
- const config = container.get<DbConfig>({ key: 'config.database' });
579
- return new Pool({
580
- host: config.host,
581
- port: config.port,
582
- max: config.poolSize,
583
- });
135
+ throw getError({
136
+ message: { text: 'User not found', code: 'user.not_found', args: { id: 42 } },
137
+ statusCode: 404,
584
138
  });
585
139
  ```
586
140
 
587
- **Class provider (implements `IProvider<T>`):**
588
-
589
- ```typescript
590
- import { IProvider, Container } from '@venizia/ignis-inversion';
591
-
592
- class DatabasePoolProvider implements IProvider<Pool> {
593
- value(container: Container): Pool {
594
- const config = container.get<DbConfig>({ key: 'config.database' });
595
- return new Pool({
596
- host: config.host,
597
- port: config.port,
598
- max: config.poolSize,
599
- });
600
- }
601
- }
602
-
603
- container.bind({ key: 'datasources.Pool' }).toProvider(DatabasePoolProvider);
604
- ```
605
-
606
- Class providers are instantiated by the container (via `container.instantiate`), so they can themselves use DI if needed.
607
-
608
- ---
609
-
610
- ### Optional Dependencies
611
-
612
- Mark a dependency as optional to receive `undefined` instead of an error when the binding does not exist.
613
-
614
- **In `@inject`:**
615
-
616
- ```typescript
617
- class MyService {
618
- constructor(
619
- @inject({ key: 'services.Analytics', isOptional: true })
620
- private analytics?: AnalyticsService,
621
- ) {}
622
- }
623
- ```
624
-
625
- **In `container.get`:**
626
-
627
- ```typescript
628
- // Throws if not bound
629
- const svc = container.get<MyService>({ key: 'services.MyService' });
630
-
631
- // Returns undefined if not bound
632
- const svc = container.get<MyService>({ key: 'services.MyService', isOptional: true });
633
- ```
634
-
635
- ---
636
-
637
- ### Singleton vs Transient Scoping
638
-
639
- | Scope | Behavior | Use Case |
640
- |:------|:---------|:---------|
641
- | `transient` (default) | New instance created on every `get()` call | Stateful per-request services, short-lived objects |
642
- | `singleton` | Cached after first resolution; same instance returned thereafter | Database pools, configuration, shared utilities |
643
-
644
- ```typescript
645
- // Transient (default) -- new instance each time
646
- container.bind({ key: 'services.RequestContext' }).toClass(RequestContext);
647
-
648
- // Singleton -- cached after first resolution
649
- container
650
- .bind({ key: 'datasources.Database' })
651
- .toClass(DatabaseDataSource)
652
- .setScope('singleton');
653
- ```
654
-
655
- **Cache management:**
656
-
657
- ```typescript
658
- // Clear a single binding's cached singleton
659
- const binding = container.getBinding({ key: 'datasources.Database' });
660
- binding?.clearCache();
661
-
662
- // Clear ALL singleton caches (bindings remain registered)
663
- container.clear();
664
-
665
- // Remove all bindings entirely
666
- container.reset();
667
- ```
668
-
669
- **Scope constants:**
670
-
671
- ```typescript
672
- import { BindingScopes } from '@venizia/ignis-inversion';
673
-
674
- BindingScopes.SINGLETON; // 'singleton'
675
- BindingScopes.TRANSIENT; // 'transient'
676
- ```
677
-
678
- ---
679
-
680
- ### Tag-Based Discovery
681
-
682
- Tags enable querying the container for groups of related bindings. Namespace auto-tagging handles the common case automatically, and you can add custom tags for more granular control.
683
-
684
- ```typescript
685
- // Auto-tagged by namespace
686
- container.bind({ key: 'controllers.UserController' }).toClass(UserController);
687
- container.bind({ key: 'controllers.OrderController' }).toClass(OrderController);
688
-
689
- // Custom tags
690
- container
691
- .bind({ key: 'services.EmailNotifier' })
692
- .toClass(EmailNotifier)
693
- .setTags('notifiers', 'async');
694
-
695
- container
696
- .bind({ key: 'services.SmsNotifier' })
697
- .toClass(SmsNotifier)
698
- .setTags('notifiers', 'async');
699
-
700
- // Find all controllers
701
- const controllerBindings = container.findByTag({ tag: 'controllers' });
702
-
703
- // Find all notifiers
704
- const notifierBindings = container.findByTag({ tag: 'notifiers' });
141
+ `getError` returns an `ApplicationError` - a real `Error` subclass carrying `statusCode`, an
142
+ optional `extra` bag, and `normalized`, which is always the same three fields:
705
143
 
706
- // Find notifiers, excluding a specific one
707
- const filtered = container.findByTag({
708
- tag: 'notifiers',
709
- exclude: ['services.SmsNotifier'],
710
- });
144
+ | Field | Meaning |
145
+ | :--- | :--- |
146
+ | `text` | The human-readable message |
147
+ | `code` | A dotted, lower snake_case message code. Falls back to `MessageCode.DEFAULT` (`core.system_error`) |
148
+ | `args` | Interpolation arguments, `{}` when there are none |
711
149
 
712
- // Resolve all found bindings
713
- const notifiers = notifierBindings.map(b => b.getValue(container));
714
- ```
150
+ `message` also accepts a bare string, which becomes `text`. Unknown top-level keys are swept into
151
+ `extra`, so a throw site can attach context the framework does not model.
715
152
 
716
- **Tag inspection:**
153
+ > [!IMPORTANT]
154
+ > Check errors with `isApplicationError(error)`, never `instanceof ApplicationError`. Several
155
+ > packages can carry their own copy of the class, so `instanceof` fails across package boundaries.
717
156
 
718
- ```typescript
719
- const binding = container.getBinding({ key: 'services.EmailNotifier' });
720
- binding?.hasTag('notifiers'); // true
721
- binding?.getTags(); // ['services', 'notifiers', 'async']
722
- ```
157
+ Also exported: `ApplicationError`, `MessageCode`, `ErrorScopes`, and the `TError*` types. Catalogued
158
+ errors are declared as `TErrorDefinition` objects and thrown with `getError({ error: Definition })`.
723
159
 
724
- ---
160
+ ## Rules that will burn you
725
161
 
726
- ### Type Guards and Utilities
162
+ **Every constructor parameter of a container-instantiated class must carry `@inject`.** There is no
163
+ channel for the container to supply an undecorated one, so a mixed constructor is refused at
164
+ instantiation with `[Class] Constructor parameter N has no @inject`. Options a class needs go into
165
+ `super({ ... })` or onto a binding, never as a bare parameter.
727
166
 
728
- The package exports several type guard functions for runtime type checking:
167
+ **This package keeps a dual CJS + ESM build on purpose.** Frontend consumers import it into browser
168
+ bundles, so `dist/esm` is load-bearing. Do not "simplify" the build to ESM-only or CJS-only.
729
169
 
730
- ```typescript
731
- import { isClass, isClassProvider, isClassConstructor } from '@venizia/ignis-inversion';
170
+ **`import 'reflect-metadata'` once, at your entrypoint.** The package imports it, but a consumer
171
+ that loads two copies gets two metadata stores and silently empty injection.
732
172
 
733
- // Check if a value is a class (has a prototype)
734
- isClass(UserService); // true
735
- isClass(() => {}); // false
173
+ **Singleton caching keys on `undefined`.** A singleton binding that resolves to `undefined` is
174
+ re-resolved on every `get` rather than cached.
736
175
 
737
- // Check if a value is an IProvider class (has prototype.value as function)
738
- isClassProvider(DatabasePoolProvider); // true
739
- isClassProvider((c) => new Pool()); // false
740
-
741
- // Check if a function is a named class constructor
742
- isClassConstructor(UserService); // true
743
- isClassConstructor(() => {}); // false
744
- ```
745
-
746
- ---
747
-
748
- ## Container Lifecycle
749
-
750
- ### Create, Bind, Resolve, Teardown
751
-
752
- A container goes through a predictable lifecycle:
753
-
754
- ```typescript
755
- import { Container } from '@venizia/ignis-inversion';
756
-
757
- // 1. CREATE -- Instantiate the container
758
- const container = new Container({ scope: 'MyApp' });
759
-
760
- // 2. BIND -- Register all dependencies
761
- container.bind({ key: 'config.port' }).toValue(3000);
762
- container.bind({ key: 'services.Logger' }).toClass(ConsoleLogger).setScope('singleton');
763
- container.bind({ key: 'services.UserService' }).toClass(UserService);
764
-
765
- // 3. RESOLVE -- Get instances (triggers instantiation + injection)
766
- const userService = container.get<UserService>({ key: 'services.UserService' });
767
- const port = container.get<number>({ key: 'config.port' });
768
-
769
- // 4. INSPECT -- Query what is registered
770
- container.isBound({ key: 'services.Logger' }); // true
771
- const loggerBinding = container.getBinding({ key: 'services.Logger' });
772
- const allServices = container.findByTag({ tag: 'services' });
773
-
774
- // 5. MODIFY -- Add or remove bindings at runtime
775
- container.unbind({ key: 'services.UserService' });
776
- container.bind({ key: 'services.UserService' }).toClass(EnhancedUserService);
777
-
778
- // 6. TEARDOWN -- Clean up
779
- container.clear(); // Clear singleton caches, keep bindings
780
- container.reset(); // Remove all bindings
781
- ```
782
-
783
- ---
784
-
785
- ### clear() vs reset()
786
-
787
- These two methods serve very different purposes:
788
-
789
- | Method | Bindings | Singleton Caches | Use Case |
790
- |:-------|:---------|:-----------------|:---------|
791
- | `clear()` | Kept | Cleared | Refresh singleton instances (e.g., reconnect a database pool) without losing the binding configuration |
792
- | `reset()` | Removed | Removed (implicitly) | Full teardown -- the container returns to an empty state |
793
-
794
- **`clear()` in detail:**
795
-
796
- Iterates over every binding in the container and calls `binding.clearCache()` on each. After calling `clear()`, the next `get()` call on a singleton binding will create a fresh instance and cache it again.
797
-
798
- ```typescript
799
- // Singleton is cached
800
- const db1 = container.get<Database>({ key: 'datasources.Database' });
801
- const db2 = container.get<Database>({ key: 'datasources.Database' });
802
- console.log(db1 === db2); // true
803
-
804
- // Clear caches
805
- container.clear();
806
-
807
- // Next resolution creates a new instance
808
- const db3 = container.get<Database>({ key: 'datasources.Database' });
809
- console.log(db1 === db3); // false
810
- ```
811
-
812
- **`reset()` in detail:**
813
-
814
- Calls `this.bindings.clear()` on the internal `Map`, removing all bindings entirely. Any subsequent `get()` call will throw `"Binding key: X is not bounded in context!"` unless new bindings are registered.
815
-
816
- ```typescript
817
- container.reset();
818
- container.isBound({ key: 'services.Logger' }); // false
819
- container.get({ key: 'services.Logger' }); // throws ApplicationError
820
- ```
821
-
822
- ---
823
-
824
- ## Binding Resolution Internals
825
-
826
- Understanding how the container resolves bindings is essential for debugging DI issues.
827
-
828
- ### Two-Phase Instantiation Walkthrough
829
-
830
- When you call `container.get({ key: 'services.UserService' })` and the binding is configured with `toClass(UserService)`, the following happens step by step:
831
-
832
- ```
833
- container.get({ key: 'services.UserService' })
834
- |
835
- +--> container.getBinding({ key: 'services.UserService' })
836
- | returns Binding<UserService>
837
- |
838
- +--> binding.getValue(container)
839
- |
840
- +--> Check scope: if singleton and cached, return cached value immediately
841
- |
842
- +--> Resolver type is CLASS, so:
843
- | container.instantiate(UserService)
844
- |
845
- | === PHASE 1: Constructor Injection ===
846
- |
847
- | 1. registry.getInjectMetadata({ target: UserService })
848
- | Returns: [
849
- | { key: 'repositories.UserRepository', index: 0, isOptional: false },
850
- | { key: 'services.Logger', index: 1, isOptional: true },
851
- | ]
852
- |
853
- | 2. Sort metadata by index (ascending)
854
- | Sorted: [index:0 -> UserRepository, index:1 -> Logger]
855
- |
856
- | 3. For each metadata entry:
857
- | args[0] = container.get({ key: 'repositories.UserRepository', isOptional: false })
858
- | args[1] = container.get({ key: 'services.Logger', isOptional: true })
859
- | (Each of these may recursively trigger instantiation of other classes)
860
- |
861
- | 4. const instance = new UserService(args[0], args[1])
862
- |
863
- | === PHASE 2: Property Injection ===
864
- |
865
- | 5. registry.getPropertiesMetadata({ target: instance })
866
- | Returns: Map {
867
- | 'cache' => { bindingKey: 'services.CacheService', isOptional: true }
868
- | }
869
- |
870
- | 6. For each property:
871
- | instance['cache'] = container.get({ key: 'services.CacheService', isOptional: true })
872
- |
873
- | 7. Return instance
874
- |
875
- +--> If scope is singleton, cache: this.cached = instance
876
- |
877
- +--> Return instance
878
- ```
879
-
880
- ---
881
-
882
- ### Resolver Types and getValue() Flow
883
-
884
- The `getValue()` method on a `Binding` handles three resolver types:
885
-
886
- ```
887
- binding.getValue(container)
888
- |
889
- +--> if (singleton && cached) return cached
890
- |
891
- +--> switch (resolver.type):
892
- |
893
- | case 'value':
894
- | instance = resolver.value (direct return, no container needed)
895
- |
896
- | case 'provider':
897
- | if (isClassProvider(resolver.value)):
898
- | // Class provider: instantiate the provider class with DI, then call .value()
899
- | providerInstance = container.instantiate(resolver.value)
900
- | instance = providerInstance.value(container)
901
- | else:
902
- | // Function provider: call the function directly with container
903
- | instance = resolver.value(container)
904
- |
905
- | case 'class':
906
- | instance = container.instantiate(resolver.value)
907
- |
908
- +--> if (singleton) this.cached = instance
909
- +--> return instance
910
- ```
911
-
912
- **Key insight:** Both `class` and `provider` (class-based) resolver types require a container reference. If `getValue()` is called without a container on these types, it throws:
913
- - `[getValue] Invalid context/container to instantiate class`
914
- - `[getValue] Invalid context/container to get provider value`
915
-
916
- The `value` resolver type does not require a container -- it returns the stored value directly.
917
-
918
- ---
919
-
920
- ### Singleton Caching Internals
921
-
922
- Singleton caching is handled **per-Binding**, not per-Container. The cached value is stored as a private `cached` property on the `Binding` instance:
923
-
924
- ```typescript
925
- // Simplified internal logic
926
- getValue(container?: Container): T {
927
- // Fast path: return cached singleton
928
- if (this.bindScope === 'singleton' && this.cached !== undefined) {
929
- return this.cached;
930
- }
931
-
932
- // ... resolve instance ...
933
-
934
- // Cache for singleton scope
935
- if (this.bindScope === 'singleton') {
936
- this.cached = instance;
937
- }
938
-
939
- return instance;
940
- }
941
- ```
942
-
943
- > **Important:** The cache check uses `this.cached !== undefined`. This means if a singleton resolves to `undefined` or `null`, it will **not** be cached and will be re-resolved on every call. For value bindings that intentionally hold `undefined`, use transient scope or wrap the value.
944
-
945
- **`clearCache()` behavior:**
946
-
947
- ```typescript
948
- clearCache() {
949
- if (!this.cached) {
950
- return;
951
- }
952
- this.cached = undefined;
953
- }
954
- ```
955
-
956
- Note that `clearCache()` checks `!this.cached` (falsy check), so it will skip clearing if the cached value is already falsy (`undefined`, `null`, `0`, `''`, `false`). For typical use cases (classes, objects, pools), this is not an issue.
957
-
958
- ---
959
-
960
- ## Provider Pattern Deep Dive
961
-
962
- ### Function Providers
963
-
964
- Function providers are simple factory functions that receive the container and return a value:
965
-
966
- ```typescript
967
- container.bind({ key: 'services.DatabasePool' }).toProvider((container) => {
968
- const host = container.get<string>({ key: 'config.db.host' });
969
- const port = container.get<number>({ key: 'config.db.port' });
970
- const pool = new Pool({ host, port, max: 10 });
971
- return pool;
972
- });
973
- ```
974
-
975
- Function providers are called directly -- `provider(container)` -- with no DI on the function itself. They are best for simple factory logic where you just need to read config and create an object.
976
-
977
- ---
978
-
979
- ### Class-Based Providers (IProvider)
980
-
981
- Class-based providers implement the `IProvider<T>` interface and are themselves instantiated via the container's DI mechanism:
982
-
983
- ```typescript
984
- import { IProvider, Container, inject, injectable } from '@venizia/ignis-inversion';
985
-
986
- interface DbConfig {
987
- host: string;
988
- port: number;
989
- maxConnections: number;
990
- }
991
-
992
- @injectable({})
993
- class DatabasePoolProvider implements IProvider<Pool> {
994
- constructor(
995
- @inject({ key: 'config.database' })
996
- private config: DbConfig,
997
- ) {}
998
-
999
- value(container: Container): Pool {
1000
- return new Pool({
1001
- host: this.config.host,
1002
- port: this.config.port,
1003
- max: this.config.maxConnections,
1004
- });
1005
- }
1006
- }
1007
-
1008
- // Registration
1009
- container.bind({ key: 'config.database' }).toValue({ host: 'localhost', port: 5432, maxConnections: 10 });
1010
- container.bind({ key: 'datasources.Pool' }).toProvider(DatabasePoolProvider).setScope('singleton');
1011
-
1012
- // Resolution -- provider is instantiated with DI, then .value() is called
1013
- const pool = container.get<Pool>({ key: 'datasources.Pool' });
1014
- ```
1015
-
1016
- The container detects class-based providers using `isClassProvider()`, which checks whether the target has a `prototype.value` method. If detected, the provider is instantiated via `container.instantiate()` (which performs full two-phase DI on the provider class), and then its `value(container)` method is called.
1017
-
1018
- The full IGNIS framework also provides a `BaseProvider<T>` abstract class (in `@venizia/ignis` core) that extends `BaseHelper` and implements `IProvider<T>`, giving providers access to scoped logging.
1019
-
1020
- ---
1021
-
1022
- ### When to Use Each
1023
-
1024
- | Pattern | Use When |
1025
- |:--------|:---------|
1026
- | **Function provider** | Simple factory logic. No need for the provider itself to have injected dependencies. One-liner or few-liner creation. |
1027
- | **Class provider** | The provider needs its own injected dependencies. Complex initialization logic. Reusable across multiple bindings. Testing requires mocking the provider. |
1028
- | **`toClass()`** | The dependency is a straightforward class with `@inject` decorators. No custom factory logic needed. |
1029
- | **`toValue()`** | The value is a static constant, configuration object, or pre-constructed instance. |
1030
-
1031
- ---
1032
-
1033
- ## Scope Behavior Details
1034
-
1035
- ### Transient Behavior
1036
-
1037
- Transient is the default scope. Every call to `get()` creates a brand new instance:
1038
-
1039
- ```typescript
1040
- container.bind({ key: 'services.RequestHandler' }).toClass(RequestHandler);
1041
-
1042
- const handler1 = container.get<RequestHandler>({ key: 'services.RequestHandler' });
1043
- const handler2 = container.get<RequestHandler>({ key: 'services.RequestHandler' });
1044
-
1045
- console.log(handler1 === handler2); // false -- different instances
1046
- ```
1047
-
1048
- Transient bindings never cache. Each resolution triggers the full two-phase instantiation process. This is ideal for per-request objects, short-lived services, or stateful objects where sharing would cause bugs.
1049
-
1050
- ---
1051
-
1052
- ### Singleton Behavior
1053
-
1054
- Singleton bindings cache the instance after the first resolution:
1055
-
1056
- ```typescript
1057
- container.bind({ key: 'datasources.Postgres' }).toClass(PostgresDataSource).setScope('singleton');
1058
-
1059
- // First call: triggers instantiation, caches result
1060
- const ds1 = container.get<PostgresDataSource>({ key: 'datasources.Postgres' });
1061
-
1062
- // Second call: returns cached instance immediately (no instantiation)
1063
- const ds2 = container.get<PostgresDataSource>({ key: 'datasources.Postgres' });
1064
-
1065
- console.log(ds1 === ds2); // true -- same instance
1066
-
1067
- // Third call from a different part of the app: still the same instance
1068
- const ds3 = container.get<PostgresDataSource>({ key: 'datasources.Postgres' });
1069
- console.log(ds1 === ds3); // true
1070
- ```
1071
-
1072
- ---
1073
-
1074
- ### Cache Management
1075
-
1076
- **Clear a single binding's cache:**
1077
-
1078
- ```typescript
1079
- const binding = container.getBinding<PostgresDataSource>({ key: 'datasources.Postgres' });
1080
- binding?.clearCache();
1081
-
1082
- // Next get() will create a fresh instance
1083
- const freshDs = container.get<PostgresDataSource>({ key: 'datasources.Postgres' });
1084
- ```
1085
-
1086
- **Clear all singleton caches at once:**
1087
-
1088
- ```typescript
1089
- container.clear(); // All singleton caches are wiped, but bindings remain
1090
- ```
1091
-
1092
- **When to use `clearCache()`:**
1093
-
1094
- - Database pool needs to reconnect after a connection failure
1095
- - Configuration changed at runtime and singleton services need refreshing
1096
- - Testing: reset state between test cases without re-registering all bindings
1097
-
1098
- ---
1099
-
1100
- ## Real-World Example: Mini Application
1101
-
1102
- This example shows a complete mini-application with multiple services wired together via DI, demonstrating constructor injection, property injection, singletons, transients, and providers:
1103
-
1104
- ```typescript
1105
- import { Container, injectable, inject, IProvider } from '@venizia/ignis-inversion';
1106
-
1107
- // --- Configuration ---
1108
- interface AppConfig {
1109
- dbHost: string;
1110
- dbPort: number;
1111
- logLevel: 'debug' | 'info' | 'warn' | 'error';
1112
- }
1113
-
1114
- // --- Logger ---
1115
- @injectable({})
1116
- class Logger {
1117
- constructor(
1118
- @inject({ key: 'config' })
1119
- private config: AppConfig,
1120
- ) {}
1121
-
1122
- info(message: string) {
1123
- if (['debug', 'info'].includes(this.config.logLevel)) {
1124
- console.log(`[INFO] ${message}`);
1125
- }
1126
- }
1127
-
1128
- error(message: string) {
1129
- console.error(`[ERROR] ${message}`);
1130
- }
1131
- }
1132
-
1133
- // --- Database Connection (via provider) ---
1134
- class DatabaseConnection {
1135
- constructor(public host: string, public port: number) {}
1136
-
1137
- query(sql: string): any[] {
1138
- // Simulated query
1139
- return [{ id: 1 }];
1140
- }
1141
-
1142
- close() {
1143
- // Close the connection pool
1144
- }
1145
- }
1146
-
1147
- class DatabaseConnectionProvider implements IProvider<DatabaseConnection> {
1148
- value(container: Container): DatabaseConnection {
1149
- const config = container.get<AppConfig>({ key: 'config' });
1150
- return new DatabaseConnection(config.dbHost, config.dbPort);
1151
- }
1152
- }
1153
-
1154
- // --- Repository ---
1155
- @injectable({})
1156
- class UserRepository {
1157
- constructor(
1158
- @inject({ key: 'datasources.Database' })
1159
- private db: DatabaseConnection,
1160
-
1161
- @inject({ key: 'services.Logger' })
1162
- private logger: Logger,
1163
- ) {}
1164
-
1165
- findById(id: number) {
1166
- this.logger.info(`UserRepository.findById(${id})`);
1167
- return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
1168
- }
1169
-
1170
- findAll() {
1171
- this.logger.info('UserRepository.findAll()');
1172
- return this.db.query('SELECT * FROM users');
1173
- }
1174
-
1175
- create(data: { name: string; email: string }) {
1176
- this.logger.info(`UserRepository.create(${data.name})`);
1177
- return { id: Date.now(), ...data };
1178
- }
1179
- }
1180
-
1181
- // --- Service with property injection ---
1182
- @injectable({})
1183
- class NotificationService {
1184
- info(message: string) {
1185
- console.log(`[NOTIFICATION] ${message}`);
1186
- }
1187
- }
1188
-
1189
- @injectable({})
1190
- class UserService {
1191
- // Property injection for optional dependency
1192
- @inject({ key: 'services.NotificationService', isOptional: true })
1193
- private notifier?: NotificationService;
1194
-
1195
- constructor(
1196
- @inject({ key: 'repositories.UserRepository' })
1197
- private userRepo: UserRepository,
1198
-
1199
- @inject({ key: 'services.Logger' })
1200
- private logger: Logger,
1201
- ) {}
1202
-
1203
- getUser(id: number) {
1204
- this.logger.info(`UserService.getUser(${id})`);
1205
- return this.userRepo.findById(id);
1206
- }
1207
-
1208
- createUser(data: { name: string; email: string }) {
1209
- this.logger.info(`UserService.createUser(${data.name})`);
1210
- const user = this.userRepo.create(data);
1211
- this.notifier?.info(`User created: ${data.name}`);
1212
- return user;
1213
- }
1214
- }
1215
-
1216
- // --- Wire it all up ---
1217
- const container = new Container({ scope: 'MyApp' });
1218
-
1219
- // Configuration (value binding)
1220
- container.bind({ key: 'config' }).toValue({
1221
- dbHost: 'localhost',
1222
- dbPort: 5432,
1223
- logLevel: 'info',
1224
- } satisfies AppConfig);
1225
-
1226
- // Singletons -- shared across the application
1227
- container.bind({ key: 'services.Logger' }).toClass(Logger).setScope('singleton');
1228
- container.bind({ key: 'datasources.Database' }).toProvider(DatabaseConnectionProvider).setScope('singleton');
1229
-
1230
- // Transients -- fresh instance per resolution
1231
- container.bind({ key: 'repositories.UserRepository' }).toClass(UserRepository);
1232
- container.bind({ key: 'services.NotificationService' }).toClass(NotificationService);
1233
- container.bind({ key: 'services.UserService' }).toClass(UserService);
1234
-
1235
- // --- Use it ---
1236
- const userService = container.get<UserService>({ key: 'services.UserService' });
1237
- userService.createUser({ name: 'Alice', email: 'alice@example.com' });
1238
- // Output:
1239
- // [INFO] UserService.createUser(Alice)
1240
- // [INFO] UserRepository.create(Alice)
1241
- // [NOTIFICATION] User created: Alice
1242
-
1243
- // Verify singleton behavior
1244
- const logger1 = container.get<Logger>({ key: 'services.Logger' });
1245
- const logger2 = container.get<Logger>({ key: 'services.Logger' });
1246
- console.log(logger1 === logger2); // true
1247
-
1248
- // Query bindings by tag
1249
- const allServices = container.findByTag({ tag: 'services' });
1250
- console.log(allServices.map(b => b.key));
1251
- // ['services.Logger', 'services.NotificationService', 'services.UserService']
1252
-
1253
- // Cleanup
1254
- container.clear(); // Clear caches for reconnection
1255
- container.reset(); // Full teardown
1256
- ```
1257
-
1258
- ---
1259
-
1260
- ## Container Independence
1261
-
1262
- Each `Container` instance is completely independent. There is no parent/child container hierarchy, no container nesting, and no shared state between containers. Two separate `Container` instances have entirely separate binding registries:
1263
-
1264
- ```typescript
1265
- const container1 = new Container({ scope: 'App1' });
1266
- const container2 = new Container({ scope: 'App2' });
1267
-
1268
- container1.bind({ key: 'config.port' }).toValue(3000);
1269
- container2.bind({ key: 'config.port' }).toValue(4000);
1270
-
1271
- container1.get<number>({ key: 'config.port' }); // 3000
1272
- container2.get<number>({ key: 'config.port' }); // 4000
1273
-
1274
- container1.isBound({ key: 'config.port' }); // true
1275
- container2.isBound({ key: 'config.port' }); // true -- independent
1276
- container1.reset();
1277
- container2.isBound({ key: 'config.port' }); // still true
1278
- ```
1279
-
1280
- **However**, the `MetadataRegistry` is a **shared singleton**. Decorator metadata (`@inject`, `@injectable`) is stored globally because TypeScript decorators execute at class definition time, before any container exists. This means:
1281
-
1282
- - All containers share the same decorator metadata.
1283
- - If you define `@inject({ key: 'services.Logger' })` on a class, that metadata is visible from any container.
1284
- - Each container independently resolves the binding for `'services.Logger'` from its own registry.
1285
-
1286
- This design is intentional: metadata describes the class's **dependencies**, while the container determines **how to fulfill** those dependencies.
1287
-
1288
- ---
1289
-
1290
- ## Integration with Other IGNIS Packages
1291
-
1292
- ### How Core Uses Inversion
1293
-
1294
- The `@venizia/ignis` core package extends both `Container` and `MetadataRegistry` from inversion:
1295
-
1296
- **Container extension:**
1297
-
1298
- ```typescript
1299
- // packages/core/src/helpers/inversion/container.ts
1300
- import { Container as DIContainer } from '@venizia/ignis-inversion';
1301
-
1302
- export class Container extends DIContainer {
1303
- logger: Logger; // Adds structured logging
1304
-
1305
- constructor(opts?: { scope: string }) {
1306
- super({ scope: opts?.scope ?? Container.name });
1307
- this.logger = LoggerFactory.getLogger([opts?.scope ?? Container.name]);
1308
- }
1309
-
1310
- override getMetadataRegistry() {
1311
- return MetadataRegistry.getInstance(); // Uses core's extended registry
1312
- }
1313
- }
1314
- ```
1315
-
1316
- The core `BaseApplication` extends this enhanced `Container`, so the application itself **is** the DI container:
1317
-
1318
- ```typescript
1319
- class BaseApplication extends BootMixin(Container) {
1320
- // The application IS the container
1321
- // this.bind(), this.get(), this.findByTag() -- all available directly
1322
- }
1323
- ```
1324
-
1325
- **Decorator re-export with custom registry:**
1326
-
1327
- Core re-exports `@inject` and `@injectable` but wires them to the core-extended `MetadataRegistry`:
1328
-
1329
- ```typescript
1330
- // packages/core/src/base/metadata/injectors.ts
1331
- import { inject as coreInject, injectable as coreInjectable } from '@venizia/ignis-inversion';
1332
-
1333
- export const inject = (opts: { key: string | symbol; isOptional?: boolean }) => {
1334
- return coreInject({ ...opts, registry: MetadataRegistry.getInstance() });
1335
- };
1336
- ```
1337
-
1338
- ---
1339
-
1340
- ### How Boot Uses Inversion
1341
-
1342
- The boot package uses inversion for auto-discovery of artifacts. All booters use `@inject` for their dependencies:
1343
-
1344
- ```typescript
1345
- // packages/boot/src/booters/controller.booter.ts
1346
- import { BindingKeys, inject } from '@venizia/ignis-inversion';
1347
-
1348
- class ControllerBooter extends BaseArtifactBooter {
1349
- constructor(
1350
- @inject({ key: '@app/project_root' }) root: string,
1351
- @inject({ key: '@app/instance' }) private readonly application: IApplication,
1352
- @inject({ key: '@app/boot-options' }) bootOptions: IBootOptions,
1353
- ) { /* ... */ }
1354
-
1355
- protected override async bind(): Promise<void> {
1356
- for (const cls of this.loadedClasses) {
1357
- const key = BindingKeys.build({ namespace: 'controllers', key: cls.name });
1358
- this.application.bind({ key }).toClass(cls).setTags('controllers');
1359
- }
1360
- }
1361
- }
1362
- ```
1363
-
1364
- The `BootMixin` uses tag-based discovery to find all booters:
1365
-
1366
- ```typescript
1367
- // packages/boot/src/boot.mixin.ts
1368
- import { BindingScopes, Container } from '@venizia/ignis-inversion';
1369
-
1370
- export const BootMixin = <T extends TMixinTarget<Container>>(baseClass: T) => {
1371
- class Mixed extends baseClass {
1372
- constructor(...args: any[]) {
1373
- super(...args);
1374
- // Register booters with the 'booter' tag
1375
- this.bind({ key: 'booter.ControllerBooter' }).toClass(ControllerBooter).setTags('booter');
1376
- this.bind({ key: 'booter.ServiceBooter' }).toClass(ServiceBooter).setTags('booter');
1377
- // ...
1378
- this.bind({ key: 'bootstrapper' }).toClass(Bootstrapper).setScope(BindingScopes.SINGLETON);
1379
- }
1380
-
1381
- boot(): Promise<IBootReport> {
1382
- const bootstrapper = this.get<Bootstrapper>({ key: 'bootstrapper' });
1383
- return bootstrapper.boot({});
1384
- }
1385
- }
1386
- return Mixed;
1387
- };
1388
- ```
1389
-
1390
- The `Bootstrapper` then discovers booters via `container.findByTag({ tag: 'booter' })` and runs them through their lifecycle phases.
1391
-
1392
- ---
1393
-
1394
- ### MetadataRegistry Extension via Mixins
1395
-
1396
- The core package extends the inversion `MetadataRegistry` using mixin composition:
1397
-
1398
- ```typescript
1399
- // packages/core/src/helpers/inversion/registry.ts
1400
- import { MetadataRegistry as _MetadataRegistry } from '@venizia/ignis-inversion';
1401
-
1402
- export class MetadataRegistry extends ControllerMetadataMixin(
1403
- RepositoryMetadataMixin(ModelMetadataMixin(DatasourceMetadataMixin(_MetadataRegistry))),
1404
- ) {
1405
- private static instance: MetadataRegistry;
1406
-
1407
- static getInstance(): MetadataRegistry {
1408
- if (!MetadataRegistry.instance) {
1409
- MetadataRegistry.instance = new MetadataRegistry();
1410
- }
1411
- return MetadataRegistry.instance;
1412
- }
1413
- }
1414
- ```
1415
-
1416
- This adds domain-specific metadata methods (controller routes, repository bindings, model schemas, datasource configs) on top of the base injection metadata that inversion provides. The mixin chain is:
1417
-
1418
- ```
1419
- inversion.MetadataRegistry (inject/property/injectable metadata)
1420
- + DatasourceMetadataMixin (datasource settings)
1421
- + ModelMetadataMixin (model schemas, relations)
1422
- + RepositoryMetadataMixin (repository bindings, schema auto-discovery)
1423
- + ControllerMetadataMixin (controller metadata, route definitions)
1424
- ```
1425
-
1426
- ---
1427
-
1428
- ## Symbol.for Metadata Keys
1429
-
1430
- The `MetadataKeys` object uses `Symbol.for()` instead of plain `Symbol()`:
1431
-
1432
- ```typescript
1433
- export const MetadataKeys = {
1434
- PROPERTIES: Symbol.for('ignis:properties'),
1435
- INJECT: Symbol.for('ignis:inject'),
1436
- INJECTABLE: Symbol.for('ignis:injectable'),
1437
- };
1438
- ```
1439
-
1440
- **Why `Symbol.for()` instead of `Symbol()`:**
1441
-
1442
- `Symbol.for('ignis:properties')` uses the **global Symbol registry**. This means:
1443
-
1444
- 1. Any module in any package that calls `Symbol.for('ignis:properties')` gets the **same symbol**.
1445
- 2. This is critical for monorepo setups where `@venizia/ignis-inversion` might be resolved from different `node_modules` paths or bundled separately.
1446
- 3. Plain `Symbol('ignis:properties')` creates a **unique** symbol each time -- if two copies of the library exist (e.g., different versions), their symbols would not match, and metadata lookups would silently fail.
1447
-
1448
- **Why prefixed with `ignis:`:**
1449
-
1450
- The `ignis:` prefix acts as a namespace to avoid collisions with other libraries that might also use `Symbol.for()`. This is a common convention for libraries that store global metadata.
1451
-
1452
- ---
1453
-
1454
- ## Performance Considerations
1455
-
1456
- ### Singleton vs Transient Performance
1457
-
1458
- - **Singleton resolution (cached):** Effectively free after the first call. The `getValue()` method checks `this.cached !== undefined` and returns immediately. No container lookup, no instantiation, no metadata reading.
1459
- - **Singleton resolution (first call):** Same cost as transient -- full two-phase instantiation.
1460
- - **Transient resolution:** Every call pays the full cost: metadata lookup, dependency resolution (potentially recursive), constructor invocation, property assignment.
1461
-
1462
- > **Recommendation:** Use singleton for anything stateless or expensive to create (database connections, loggers, configuration parsers). Use transient for per-request state or objects that must not be shared.
1463
-
1464
- ### Metadata Reflection Cost
1465
-
1466
- - `Reflect.getMetadata()` is called on every class instantiation (both constructor and property phases).
1467
- - The metadata itself is computed once at class definition time (when decorators execute) and stored in the reflect-metadata backing store.
1468
- - The per-resolution cost is the reflection lookup + iterating the inject metadata array. For a class with N constructor parameters and M properties, this is O(N log N + M) (the sort for constructor params, then the property iteration).
1469
-
1470
- ### Container Lookup Cost
1471
-
1472
- - Bindings are stored in a `Map<string | symbol, Binding>`. Lookup is O(1) average.
1473
- - `findByTag()` iterates all bindings -- O(total bindings). For tag-heavy queries, this is the most expensive container operation.
1474
- - `getBinding()` with a `{ namespace, key }` object calls `BindingKeys.build()` first, which concatenates two strings. Negligible cost.
1475
-
1476
- ### Best Practices
1477
-
1478
- 1. **Register datasources as singletons** -- connection pools are expensive and must be shared.
1479
- 2. **Register services as singletons if they are stateless** -- avoids repeated instantiation.
1480
- 3. **Keep constructor parameter counts reasonable** -- 3--5 parameters is typical. More than 7 is a code smell indicating the class has too many responsibilities.
1481
- 4. **Prefer constructor injection over property injection** -- it is resolved during instantiation (one pass), while property injection requires an additional metadata lookup pass.
1482
-
1483
- ---
1484
-
1485
- ## Types
1486
-
1487
- ### Core Types
1488
-
1489
- ```typescript
1490
- // Nullable wrapper
1491
- type TNullable<T> = T | undefined | null;
1492
-
1493
- // Value that may be synchronous or a Promise
1494
- type ValueOrPromise<T> = T | Promise<T>;
1495
-
1496
- // Extract value types from an object
1497
- type ValueOf<T> = T[keyof T];
1498
-
1499
- // Class constructor types
1500
- type TConstructor<T> = new (...args: any[]) => T;
1501
- type TAbstractConstructor<T> = abstract new (...args: any[]) => T;
1502
- type TClass<T> = TConstructor<T> & { [property: string]: any };
1503
-
1504
- // Extract string/number constant values from a class
1505
- type TConstValue<T extends TClass<any>> = Extract<ValueOf<T>, string | number>;
1506
- ```
1507
-
1508
- ### DI-Specific Types
1509
-
1510
- ```typescript
1511
- // Binding scope: 'singleton' | 'transient'
1512
- type TBindingScope = TConstValue<typeof BindingScopes>;
1513
-
1514
- // Binding value type: 'class' | 'value' | 'provider'
1515
- type TBindingValueType = TConstValue<typeof BindingValueTypes>;
1516
-
1517
- // Provider interface for class-based providers
1518
- interface IProvider<T> {
1519
- value(container: Container): T;
1520
- }
1521
-
1522
- // Binding tag interface
1523
- interface IBindingTag {
1524
- [name: string]: any;
1525
- }
1526
- ```
1527
-
1528
- ### Metadata Types
1529
-
1530
- ```typescript
1531
- // Property injection metadata
1532
- interface IPropertyMetadata {
1533
- bindingKey: string | symbol;
1534
- isOptional?: boolean;
1535
- [key: string]: any;
1536
- }
1537
-
1538
- // Constructor parameter injection metadata
1539
- interface IInjectMetadata {
1540
- key: string | symbol;
1541
- index: number;
1542
- isOptional?: boolean;
1543
- }
1544
-
1545
- // @injectable decorator metadata
1546
- interface IInjectableMetadata {
1547
- scope?: TBindingScope;
1548
- tags?: Record<string, any>;
1549
- }
1550
- ```
1551
-
1552
- ### Metadata Keys
1553
-
1554
- Globally registered symbols used internally to store decorator metadata:
1555
-
1556
- ```typescript
1557
- import { MetadataKeys } from '@venizia/ignis-inversion';
1558
-
1559
- MetadataKeys.PROPERTIES; // Symbol.for('ignis:properties')
1560
- MetadataKeys.INJECT; // Symbol.for('ignis:inject')
1561
- MetadataKeys.INJECTABLE; // Symbol.for('ignis:injectable')
1562
- ```
1563
-
1564
- ---
1565
-
1566
- ## Complete Type Reference
1567
-
1568
- Every type, interface, class, constant, and function exported from `@venizia/ignis-inversion`:
1569
-
1570
- ### Classes
1571
-
1572
- ```typescript
1573
- class BaseHelper {
1574
- scope: string;
1575
- identifier: string;
1576
- constructor(opts: { scope: string; identifier?: string });
1577
- }
1578
-
1579
- class Container extends BaseHelper {
1580
- protected bindings: Map<string | symbol, Binding>;
1581
-
1582
- constructor(opts?: { scope: string });
1583
- getMetadataRegistry(): MetadataRegistry;
1584
- bind<T>(opts: { key: string | symbol }): Binding<T>;
1585
- isBound(opts: { key: string | symbol }): boolean;
1586
- getBinding<T>(opts: { key: string | symbol | { namespace: string; key: string } }): TNullable<Binding<T>>;
1587
- unbind(opts: { key: string | symbol }): boolean;
1588
- set<T>(opts: { binding: Binding<T> }): void;
1589
- get<T>(opts: { key: string | symbol | { namespace: string; key: string }; isOptional?: false }): T;
1590
- get<T>(opts: { key: string | symbol | { namespace: string; key: string }; isOptional?: boolean }): T | undefined;
1591
- gets<T extends unknown[]>(opts: {
1592
- bindings: { [K in keyof T]: { key: string | symbol | { namespace: string; key: string }; isOptional?: boolean } };
1593
- }): { [K in keyof T]: T[K] | undefined };
1594
- resolve<T>(cls: TClass<T>): T;
1595
- instantiate<T>(cls: TClass<T>): T;
1596
- findByTag<T = any>(opts: { tag: string; exclude?: Array<string> | Set<string> }): Binding<T>[];
1597
- clear(): void;
1598
- reset(): void;
1599
- }
1600
-
1601
- class Binding<T = any> extends BaseHelper {
1602
- key: string;
1603
-
1604
- constructor(opts: { key: string });
1605
- static bind<T = any>(opts: { key: string }): Binding<T>;
1606
- toClass(value: TClass<T>): this;
1607
- toValue(value: T): this;
1608
- toProvider(value: (<C extends Container>(container: C) => T) | TClass<IProvider<T>>): this;
1609
- getBindingMeta(opts: { type: TConstValue<typeof BindingValueTypes> }): any;
1610
- setScope(scope: TBindingScope): this;
1611
- setTags(...tags: string[]): this;
1612
- hasTag(tag: string): boolean;
1613
- getTags(): string[];
1614
- getScope(): TBindingScope;
1615
- getValue(container?: Container): T;
1616
- clearCache(): void;
1617
- }
1618
-
1619
- class MetadataRegistry extends BaseHelper {
1620
- constructor();
1621
- define<Target extends object, Value = any>(opts: { target: Target; key: string | symbol; value: Value }): void;
1622
- get<Target extends object, Value = any>(opts: { target: Target; key: string | symbol }): Value | undefined;
1623
- has<Target extends object>(opts: { target: Target; key: string | symbol }): boolean;
1624
- delete<Target extends object>(opts: { target: Target; key: string | symbol }): boolean;
1625
- getKeys<Target extends object>(opts: { target: Target }): (string | symbol)[];
1626
- getMethodNames<T = any>(opts: { target: TClass<T> }): string[];
1627
- clearMetadata<T extends object>(opts: { target: T }): void;
1628
- setPropertyMetadata<T extends object>(opts: { target: T; propertyName: string | symbol; metadata: IPropertyMetadata }): void;
1629
- getPropertiesMetadata<T extends object>(opts: { target: T }): Map<string | symbol, IPropertyMetadata> | undefined;
1630
- getPropertyMetadata<T extends object>(opts: { target: T; propertyName: string | symbol }): IPropertyMetadata | undefined;
1631
- setInjectMetadata<T extends object>(opts: { target: T; index: number; metadata: IInjectMetadata }): void;
1632
- getInjectMetadata<T extends object>(opts: { target: T }): IInjectMetadata[] | undefined;
1633
- setInjectableMetadata<T extends object>(opts: { target: T; metadata: IInjectableMetadata }): void;
1634
- getInjectableMetadata<T extends object>(opts: { target: T }): IInjectableMetadata | undefined;
1635
- }
1636
-
1637
- class ApplicationError extends Error {
1638
- statusCode: number;
1639
- messageCode?: string;
1640
- constructor(opts: TError);
1641
- static getError(opts: TError): ApplicationError;
1642
- }
1643
-
1644
- class Logger {
1645
- static info(message: string, ...args: unknown[]): void;
1646
- static warn(message: string, ...args: unknown[]): void;
1647
- static error(message: string, ...args: unknown[]): void;
1648
- static debug(message: string, ...args: unknown[]): void; // Only logs when process.env.DEBUG is set
1649
- }
1650
- ```
1651
-
1652
- ### Static Classes (Constants)
1653
-
1654
- ```typescript
1655
- class BindingScopes {
1656
- static readonly SINGLETON = 'singleton'; // 'singleton'
1657
- static readonly TRANSIENT = 'transient'; // 'transient'
1658
- }
1659
-
1660
- class BindingValueTypes {
1661
- static readonly CLASS = 'class'; // 'class'
1662
- static readonly VALUE = 'value'; // 'value'
1663
- static readonly PROVIDER = 'provider'; // 'provider'
1664
- }
1665
-
1666
- class BindingKeys {
1667
- static build(opts: { namespace: string; key: string }): string;
1668
- }
1669
- ```
1670
-
1671
- ### Interfaces
1672
-
1673
- ```typescript
1674
- interface IProvider<T> {
1675
- value(container: Container): T;
1676
- }
1677
-
1678
- interface IBindingTag {
1679
- [name: string]: any;
1680
- }
1681
-
1682
- interface IPropertyMetadata {
1683
- bindingKey: string | symbol;
1684
- isOptional?: boolean;
1685
- [key: string]: any;
1686
- }
1687
-
1688
- interface IInjectMetadata {
1689
- key: string | symbol;
1690
- index: number;
1691
- isOptional?: boolean;
1692
- }
1693
-
1694
- interface IInjectableMetadata {
1695
- scope?: TBindingScope;
1696
- tags?: Record<string, any>;
1697
- }
1698
- ```
1699
-
1700
- ### Type Aliases
1701
-
1702
- ```typescript
1703
- type TNullable<T> = T | undefined | null;
1704
- type ValueOrPromise<T> = T | Promise<T>;
1705
- type ValueOf<T> = T[keyof T];
1706
- type TConstructor<T> = new (...args: any[]) => T;
1707
- type TAbstractConstructor<T> = abstract new (...args: any[]) => T;
1708
- type TClass<T> = TConstructor<T> & { [property: string]: any };
1709
- type TConstValue<T extends TClass<any>> = Extract<ValueOf<T>, string | number>;
1710
- type TBindingScope = TConstValue<typeof BindingScopes>; // 'singleton' | 'transient'
1711
- type TBindingValueType = TConstValue<typeof BindingValueTypes>; // 'class' | 'value' | 'provider'
1712
- type TError = z.infer<typeof ErrorSchema>; // { message: string; name?: string; statusCode?: number; messageCode?: string }
1713
- ```
1714
-
1715
- ### Constants
1716
-
1717
- ```typescript
1718
- const MetadataKeys: {
1719
- PROPERTIES: symbol; // Symbol.for('ignis:properties')
1720
- INJECT: symbol; // Symbol.for('ignis:inject')
1721
- INJECTABLE: symbol; // Symbol.for('ignis:injectable')
1722
- };
1723
- ```
1724
-
1725
- ### Exported Singleton
1726
-
1727
- ```typescript
1728
- const metadataRegistry: MetadataRegistry; // Pre-instantiated shared MetadataRegistry
1729
- ```
1730
-
1731
- ### Schemas
1732
-
1733
- ```typescript
1734
- const ErrorSchema: z.ZodObject<{
1735
- name: z.ZodOptional<z.ZodString>;
1736
- statusCode: z.ZodOptional<z.ZodNumber>;
1737
- messageCode: z.ZodOptional<z.ZodString>;
1738
- message: z.ZodString;
1739
- }>;
1740
- ```
1741
-
1742
- ### Functions
1743
-
1744
- ```typescript
1745
- function injectable(metadata: IInjectableMetadata, registry?: MetadataRegistry): ClassDecorator;
1746
- function inject(opts: { key: string | symbol; isOptional?: boolean; registry?: MetadataRegistry }): PropertyDecorator & ParameterDecorator;
1747
- function isClass<T>(target: any): target is TClass<T>;
1748
- function isClassProvider<T>(target: any): target is TClass<IProvider<T>>;
1749
- function isClassConstructor(fn: Function): boolean;
1750
- function getError(opts: TError): ApplicationError;
1751
- ```
1752
-
1753
- ---
1754
-
1755
- ## Error Handling
1756
-
1757
- All container errors are thrown as `ApplicationError` instances, which extend the native `Error` class with additional context.
1758
-
1759
- ```typescript
1760
- import { ApplicationError, getError, ErrorSchema } from '@venizia/ignis-inversion';
1761
-
1762
- // ApplicationError structure
1763
- class ApplicationError extends Error {
1764
- statusCode: number; // HTTP status code (default: 400)
1765
- messageCode?: string; // Machine-readable error code
1766
- }
1767
-
1768
- // Factory function
1769
- const err = getError({
1770
- message: 'Binding not found',
1771
- statusCode: 404,
1772
- messageCode: 'BINDING_NOT_FOUND',
1773
- });
1774
-
1775
- // Static factory
1776
- const err2 = ApplicationError.getError({
1777
- message: 'Something went wrong',
1778
- });
1779
-
1780
- // Zod schema for validation
1781
- ErrorSchema.parse({
1782
- message: 'Invalid input',
1783
- statusCode: 400,
1784
- });
1785
- ```
1786
-
1787
- ### Common Error Scenarios
1788
-
1789
- **1. Resolving an unbound key:**
1790
-
1791
- ```typescript
1792
- container.get({ key: 'services.Unknown' });
1793
- // throws: ApplicationError { message: 'Binding key: services.Unknown is not bounded in context!' }
1794
- ```
1795
-
1796
- **2. Resolving an unbound optional key:**
1797
-
1798
- ```typescript
1799
- const result = container.get({ key: 'services.Unknown', isOptional: true });
1800
- // result === undefined (no error thrown)
1801
- ```
1802
-
1803
- **3. Class binding resolved without container context:**
1804
-
1805
- This happens internally if you call `binding.getValue()` without passing a container on a class or provider binding.
1806
-
1807
- ```typescript
1808
- const binding = Binding.bind({ key: 'test' });
1809
- binding.toClass(SomeClass);
1810
- binding.getValue(); // no container passed
1811
- // throws: ApplicationError { message: '[getValue] Invalid context/container to instantiate class | type: class | key: test' }
1812
- ```
1813
-
1814
- **4. Provider binding resolved without container:**
1815
-
1816
- ```typescript
1817
- const binding = Binding.bind({ key: 'test' });
1818
- binding.toProvider((container) => 'value');
1819
- binding.getValue();
1820
- // throws: ApplicationError { message: '[getValue] Invalid context/container to get provider value | type: provider | key: test' }
1821
- ```
1822
-
1823
- **5. Accessing wrong resolver type via getBindingMeta:**
1824
-
1825
- ```typescript
1826
- const binding = Binding.bind({ key: 'test' });
1827
- binding.toValue('hello');
1828
- binding.getBindingMeta({ type: 'class' });
1829
- // throws: ApplicationError { message: '[getBindingMeta] Invalid resolver type, only value is allowd | resolverType: value | optType: class' }
1830
- ```
1831
-
1832
- **6. Invalid binding key type in getBinding:**
1833
-
1834
- ```typescript
1835
- container.getBinding({ key: 123 as any });
1836
- // throws: ApplicationError { message: '[getBinding] Invalid binding key type | opts: 123 | allowed: [string, symbol, { namespace: string, key: string }]' }
1837
- ```
1838
-
1839
- **7. @inject used in wrong context:**
1840
-
1841
- ```typescript
1842
- // If @inject is somehow applied to something that is neither a property nor a constructor parameter:
1843
- // throws: ApplicationError { message: '@inject decorator can only be used on class properties or constructor parameters' }
1844
- ```
1845
-
1846
- **8. BindingKeys.build with empty key:**
1847
-
1848
- ```typescript
1849
- BindingKeys.build({ namespace: 'services', key: '' });
1850
- // throws: ApplicationError { message: '[BindingKeys][build] Invalid key to build | key: ' }
1851
- ```
1852
-
1853
- ---
1854
-
1855
- ### Error Catalog
1856
-
1857
- | Scenario | Error Message | Root Cause |
1858
- |:---------|:--------------|:-----------|
1859
- | Unbound key | `Binding key: {key} is not bounded in context!` | The key was never registered via `bind()` |
1860
- | Class without container | `[getValue] Invalid context/container to instantiate class` | `getValue()` called without passing container |
1861
- | Provider without container | `[getValue] Invalid context/container to get provider value` | `getValue()` called without passing container |
1862
- | Wrong resolver type | `[getBindingMeta] Invalid resolver type, only {actual} is allowd` | Accessing binding metadata with wrong type |
1863
- | Invalid key type | `[getBinding] Invalid binding key type` | Key is not string, symbol, or object |
1864
- | Invalid decorator usage | `@inject decorator can only be used on class properties or constructor parameters` | Decorator applied to wrong target |
1865
- | Empty binding key build | `[BindingKeys][build] Invalid key to build` | `BindingKeys.build()` called with empty key |
1866
-
1867
- > **Note on circular dependencies:** The container does **not** detect circular dependencies at bind-time or resolution-time. If class A depends on class B, and class B depends on class A (both via constructor injection), resolution will enter infinite recursion and crash with a stack overflow. To break circular dependencies, use property injection on one side, or introduce a provider that defers resolution.
1868
-
1869
- ---
1870
-
1871
- ## API Reference
1872
-
1873
- ### Container API
1874
-
1875
- | Method | Signature | Description |
1876
- |:-------|:----------|:------------|
1877
- | `constructor` | `new Container(opts?: { scope: string })` | Create a new container with optional debug scope |
1878
- | `bind` | `bind<T>(opts: { key: string \| symbol }): Binding<T>` | Create and register a new binding |
1879
- | `get` | `get<T>(opts: { key: string \| symbol \| { namespace, key }, isOptional?: boolean }): T` | Resolve a dependency by key |
1880
- | `gets` | `gets<T>(opts: { bindings: Array<{ key, isOptional? }> }): T[]` | Resolve multiple dependencies at once |
1881
- | `getBinding` | `getBinding<T>(opts: { key: string \| symbol \| { namespace, key } }): Binding<T> \| undefined` | Get the raw Binding object without resolving |
1882
- | `set` | `set<T>(opts: { binding: Binding<T> }): void` | Insert an externally-created binding |
1883
- | `isBound` | `isBound(opts: { key: string \| symbol }): boolean` | Check if a key has a registered binding |
1884
- | `unbind` | `unbind(opts: { key: string \| symbol }): boolean` | Remove a binding |
1885
- | `resolve` | `resolve<T>(cls: TClass<T>): T` | Instantiate a class with DI (alias for `instantiate`) |
1886
- | `instantiate` | `instantiate<T>(cls: TClass<T>): T` | Instantiate a class, injecting constructor params and properties |
1887
- | `findByTag` | `findByTag<T>(opts: { tag: string, exclude?: string[] \| Set<string> }): Binding<T>[]` | Find all bindings matching a tag |
1888
- | `clear` | `clear(): void` | Clear all singleton caches (bindings remain) |
1889
- | `reset` | `reset(): void` | Remove all bindings |
1890
- | `getMetadataRegistry` | `getMetadataRegistry(): MetadataRegistry` | Access the global metadata registry |
1891
-
1892
- ### Binding API
1893
-
1894
- | Method | Signature | Description |
1895
- |:-------|:----------|:------------|
1896
- | `constructor` | `new Binding<T>(opts: { key: string })` | Create a binding (prefer `container.bind()`) |
1897
- | `Binding.bind` | `static bind<T>(opts: { key: string }): Binding<T>` | Static factory method |
1898
- | `toClass` | `toClass(value: TClass<T>): this` | Resolve by instantiating a class with DI |
1899
- | `toValue` | `toValue(value: T): this` | Resolve by returning a static value |
1900
- | `toProvider` | `toProvider(value: ((container) => T) \| TClass<IProvider<T>>): this` | Resolve via factory function or provider class |
1901
- | `setScope` | `setScope(scope: 'singleton' \| 'transient'): this` | Set the binding scope |
1902
- | `setTags` | `setTags(...tags: string[]): this` | Add tags to the binding |
1903
- | `hasTag` | `hasTag(tag: string): boolean` | Check if binding has a specific tag |
1904
- | `getTags` | `getTags(): string[]` | Get all tags |
1905
- | `getScope` | `getScope(): TBindingScope` | Get the current scope |
1906
- | `getValue` | `getValue(container?: Container): T` | Resolve the binding value |
1907
- | `getBindingMeta` | `getBindingMeta(opts: { type: TBindingValueType }): any` | Get the raw resolver value (with type assertion) |
1908
- | `clearCache` | `clearCache(): void` | Clear the singleton cache for this binding |
1909
-
1910
- ### MetadataRegistry API
1911
-
1912
- | Method | Signature | Description |
1913
- |:-------|:----------|:------------|
1914
- | `define` | `define(opts: { target, key, value }): void` | Store metadata on a target |
1915
- | `get` | `get(opts: { target, key }): Value \| undefined` | Retrieve metadata |
1916
- | `has` | `has(opts: { target, key }): boolean` | Check if metadata exists |
1917
- | `delete` | `delete(opts: { target, key }): boolean` | Remove metadata |
1918
- | `getKeys` | `getKeys(opts: { target }): (string \| symbol)[]` | List all metadata keys |
1919
- | `getMethodNames` | `getMethodNames(opts: { target: TClass }): string[]` | List non-constructor method names |
1920
- | `clearMetadata` | `clearMetadata(opts: { target }): void` | Remove all metadata from a target |
1921
- | `setInjectMetadata` | `setInjectMetadata(opts: { target, index, metadata }): void` | Store constructor `@inject` metadata |
1922
- | `getInjectMetadata` | `getInjectMetadata(opts: { target }): IInjectMetadata[] \| undefined` | Retrieve constructor injection metadata |
1923
- | `setPropertyMetadata` | `setPropertyMetadata(opts: { target, propertyName, metadata }): void` | Store property `@inject` metadata |
1924
- | `getPropertiesMetadata` | `getPropertiesMetadata(opts: { target }): Map<string \| symbol, IPropertyMetadata> \| undefined` | Retrieve all property metadata |
1925
- | `getPropertyMetadata` | `getPropertyMetadata(opts: { target, propertyName }): IPropertyMetadata \| undefined` | Retrieve metadata for a single property |
1926
- | `setInjectableMetadata` | `setInjectableMetadata(opts: { target, metadata }): void` | Store `@injectable` metadata |
1927
- | `getInjectableMetadata` | `getInjectableMetadata(opts: { target }): IInjectableMetadata \| undefined` | Retrieve `@injectable` metadata |
1928
-
1929
- ### Decorators API
1930
-
1931
- | Decorator | Signature | Description |
1932
- |:----------|:----------|:------------|
1933
- | `@injectable` | `injectable(metadata: IInjectableMetadata, registry?: MetadataRegistry): ClassDecorator` | Mark a class as injectable |
1934
- | `@inject` | `inject(opts: { key: string \| symbol, isOptional?: boolean, registry?: MetadataRegistry })` | Inject a dependency into a constructor parameter or property |
1935
-
1936
- ### Utility Functions
1937
-
1938
- | Function | Signature | Description |
1939
- |:---------|:----------|:------------|
1940
- | `isClass` | `isClass<T>(target: any): target is TClass<T>` | Check if a value is a class |
1941
- | `isClassProvider` | `isClassProvider<T>(target: any): target is TClass<IProvider<T>>` | Check if a value is an `IProvider` class |
1942
- | `isClassConstructor` | `isClassConstructor(fn: Function): boolean` | Check if a function is a named class constructor |
1943
- | `getError` | `getError(opts: { message, statusCode?, messageCode? }): ApplicationError` | Create an ApplicationError |
1944
- | `BindingKeys.build` | `BindingKeys.build(opts: { namespace: string, key: string }): string` | Build a namespaced binding key |
1945
-
1946
- ---
1947
-
1948
- ## Migration Guide
1949
-
1950
- ### From InversifyJS
1951
-
1952
- InversifyJS uses similar concepts but with different API shapes. Here is a mapping:
1953
-
1954
- | InversifyJS | IGNIS Inversion | Notes |
1955
- |:------------|:----------------|:------|
1956
- | `@injectable()` | `@injectable({})` | IGNIS requires an options object (can be empty) |
1957
- | `@inject(TYPES.Logger)` | `@inject({ key: 'services.Logger' })` | IGNIS uses options objects, supports string or symbol keys |
1958
- | `container.bind<T>(TYPES.Logger).to(Logger)` | `container.bind({ key: 'services.Logger' }).toClass(Logger)` | Fluent chain on options-based `bind()` |
1959
- | `container.bind<T>(TYPES.Logger).to(Logger).inSingletonScope()` | `container.bind({ key: 'services.Logger' }).toClass(Logger).setScope('singleton')` | Scope via `setScope()` |
1960
- | `container.bind<T>(TYPES.Config).toConstantValue(val)` | `container.bind({ key: 'config' }).toValue(val)` | `toValue` instead of `toConstantValue` |
1961
- | `container.bind<T>(TYPES.Pool).toDynamicValue((ctx) => ...)` | `container.bind({ key: 'pool' }).toProvider((container) => ...)` | Provider receives container directly, no request context |
1962
- | `container.get<T>(TYPES.Logger)` | `container.get<T>({ key: 'services.Logger' })` | Options object for `get()` |
1963
- | `@optional()` + `@inject()` | `@inject({ key: '...', isOptional: true })` | Single decorator with `isOptional` flag |
1964
- | `container.isBound(TYPES.Logger)` | `container.isBound({ key: 'services.Logger' })` | Options object |
1965
- | `container.unbind(TYPES.Logger)` | `container.unbind({ key: 'services.Logger' })` | Options object |
1966
- | `@tagged('name', value)` | `binding.setTags('name')` | Tags are set on bindings, not decorators |
1967
- | `container.getAll<T>(TYPES.Plugin)` | `container.findByTag({ tag: 'plugins' }).map(b => b.getValue(container))` | Use tag-based discovery + manual resolution |
1968
-
1969
- **Example migration:**
1970
-
1971
- ```typescript
1972
- // InversifyJS
1973
- const TYPES = {
1974
- Logger: Symbol.for('Logger'),
1975
- UserRepo: Symbol.for('UserRepo'),
1976
- UserService: Symbol.for('UserService'),
1977
- };
1978
-
1979
- @injectable()
1980
- class UserService {
1981
- constructor(
1982
- @inject(TYPES.UserRepo) private repo: UserRepository,
1983
- @inject(TYPES.Logger) private logger: Logger,
1984
- ) {}
1985
- }
1986
-
1987
- const container = new InversifyContainer();
1988
- container.bind<Logger>(TYPES.Logger).to(Logger).inSingletonScope();
1989
- container.bind<UserRepository>(TYPES.UserRepo).to(UserRepository);
1990
- container.bind<UserService>(TYPES.UserService).to(UserService);
1991
- const svc = container.get<UserService>(TYPES.UserService);
1992
- ```
1993
-
1994
- ```typescript
1995
- // IGNIS Inversion equivalent
1996
- @injectable({})
1997
- class UserService {
1998
- constructor(
1999
- @inject({ key: 'repositories.UserRepository' }) private repo: UserRepository,
2000
- @inject({ key: 'services.Logger' }) private logger: Logger,
2001
- ) {}
2002
- }
2003
-
2004
- const container = new Container();
2005
- container.bind({ key: 'services.Logger' }).toClass(Logger).setScope('singleton');
2006
- container.bind({ key: 'repositories.UserRepository' }).toClass(UserRepository);
2007
- container.bind({ key: 'services.UserService' }).toClass(UserService);
2008
- const svc = container.get<UserService>({ key: 'services.UserService' });
2009
- ```
2010
-
2011
- ---
2012
-
2013
- ### From tsyringe
2014
-
2015
- tsyringe uses a token-based approach with a global container. Here is the mapping:
2016
-
2017
- | tsyringe | IGNIS Inversion | Notes |
2018
- |:---------|:----------------|:------|
2019
- | `@injectable()` | `@injectable({})` | Similar, options object required |
2020
- | `@inject('token')` | `@inject({ key: 'token' })` | Options object |
2021
- | `container.register('token', { useClass: Cls })` | `container.bind({ key: 'token' }).toClass(Cls)` | Fluent API instead of config object |
2022
- | `container.register('token', { useValue: val })` | `container.bind({ key: 'token' }).toValue(val)` | |
2023
- | `container.register('token', { useFactory: fn })` | `container.bind({ key: 'token' }).toProvider(fn)` | Factory/provider |
2024
- | `container.resolve(Cls)` | `container.resolve(Cls)` or `container.instantiate(Cls)` | Nearly identical |
2025
- | `@singleton()` | `@injectable({})` + `.setScope('singleton')` | Scope on binding, not decorator |
2026
- | `container.createChildContainer()` | N/A | IGNIS has no child containers |
2027
- | `@injectAll('token')` | `container.findByTag({ tag }).map(b => b.getValue(container))` | Use tag discovery |
2028
-
2029
- **Key differences from tsyringe:**
2030
-
2031
- - IGNIS does not have a global container -- you always create explicit `Container` instances.
2032
- - IGNIS has no child containers. Each container is independent.
2033
- - IGNIS uses namespace-based string keys with auto-tagging, rather than string/symbol tokens.
2034
- - Property injection is supported natively in IGNIS (tsyringe only supports constructor injection).
2035
-
2036
- ---
2037
-
2038
- ## Troubleshooting
2039
-
2040
- ### Forgetting `experimentalDecorators` or `emitDecoratorMetadata`
2041
-
2042
- **Symptom:** `@inject` and `@injectable` decorators have no effect. Dependencies are `undefined` at runtime. No error is thrown.
2043
-
2044
- **Fix:** Add both flags to your `tsconfig.json`:
2045
-
2046
- ```json
2047
- {
2048
- "compilerOptions": {
2049
- "experimentalDecorators": true,
2050
- "emitDecoratorMetadata": true
2051
- }
2052
- }
2053
- ```
2054
-
2055
- Both are required. `experimentalDecorators` enables decorator syntax, while `emitDecoratorMetadata` causes TypeScript to emit `Reflect.metadata()` calls that store parameter type information.
2056
-
2057
- ---
2058
-
2059
- ### Wrong Binding Key Format
2060
-
2061
- **Symptom:** `Binding key: X is not bounded in context!` even though you registered the binding.
2062
-
2063
- **Common causes:**
2064
-
2065
- - Mismatched key strings (typo, different casing).
2066
- - Using `Symbol()` instead of `Symbol.for()` -- each `Symbol()` call creates a unique symbol, so `Symbol('key') !== Symbol('key')`.
2067
- - Registering with `'services.UserService'` but resolving with `'service.UserService'` (missing 's').
2068
-
2069
- **Fix:** Use `container.isBound({ key })` to verify the exact key exists. Consider using `BindingKeys.build()` to construct keys consistently.
2070
-
2071
- ---
2072
-
2073
- ### Optional vs Required Dependencies
2074
-
2075
- **Symptom:** Application crashes with `Binding key: X is not bounded in context!` for a dependency that should be optional.
2076
-
2077
- **Fix:** Add `isOptional: true` to the `@inject` options:
2078
-
2079
- ```typescript
2080
- @inject({ key: 'services.Analytics', isOptional: true })
2081
- private analytics?: AnalyticsService;
2082
- ```
2083
-
2084
- ---
2085
-
2086
- ### Property Injection Not Working
2087
-
2088
- **Symptom:** Property decorated with `@inject` is `undefined` at runtime.
2089
-
2090
- **Possible causes:**
2091
-
2092
- 1. Missing `experimentalDecorators` / `emitDecoratorMetadata` in tsconfig.
2093
- 2. Accessing the property in the constructor -- property injection happens **after** the constructor runs. If you need the dependency in the constructor, use constructor injection instead.
2094
- 3. The class was instantiated with `new MyClass()` instead of `container.get()` or `container.instantiate()`. Manual instantiation bypasses the DI container entirely.
2095
-
2096
- ---
2097
-
2098
- ### Singleton Returns Different Instances
2099
-
2100
- **Symptom:** A binding set to `setScope('singleton')` returns different instances.
2101
-
2102
- **Possible causes:**
2103
-
2104
- 1. The key string does not match exactly between `bind()` and `get()`.
2105
- 2. `container.clear()` was called between resolutions, which wipes all singleton caches.
2106
- 3. The binding was `unbind()`-ed and re-`bind()`-ed, creating a new `Binding` instance with no cache.
2107
-
2108
- ---
2109
-
2110
- ### Circular Dependencies
2111
-
2112
- **Symptom:** Stack overflow error during resolution.
2113
-
2114
- **Cause:** Class A constructor-injects Class B, and Class B constructor-injects Class A.
2115
-
2116
- **Fix:** Break the cycle by using property injection on one side:
2117
-
2118
- ```typescript
2119
- // Instead of:
2120
- class A {
2121
- constructor(@inject({ key: 'B' }) private b: B) {}
2122
- }
2123
- class B {
2124
- constructor(@inject({ key: 'A' }) private a: A) {}
2125
- }
2126
-
2127
- // Use property injection on one side:
2128
- class A {
2129
- constructor(@inject({ key: 'B' }) private b: B) {}
2130
- }
2131
- class B {
2132
- @inject({ key: 'A' })
2133
- private a!: A; // Property injection breaks the cycle
2134
- }
2135
- ```
2136
-
2137
- Alternatively, use a provider to defer resolution:
2138
-
2139
- ```typescript
2140
- container.bind({ key: 'A' }).toProvider((container) => {
2141
- const a = new A();
2142
- a.b = container.get({ key: 'B' });
2143
- return a;
2144
- });
2145
- ```
2146
-
2147
- ---
2148
-
2149
- ### Debug Logging
2150
-
2151
- Set the `DEBUG` environment variable to enable debug-level logging from the `MetadataRegistry`:
2152
-
2153
- ```bash
2154
- DEBUG=1 bun run start
2155
- ```
2156
-
2157
- This will print messages like:
2158
-
2159
- ```
2160
- [DEBUG] [define] Set metadata | target: UserService | key: Symbol(ignis:inject) | value: [...]
2161
- ```
2162
-
2163
- This is helpful for verifying that decorators are correctly storing metadata.
2164
-
2165
- ---
176
+ ## Links
2166
177
 
2167
- ## License
178
+ [Documentation](https://ignis.venizia.ai) &#8226;
179
+ [Dependency injection](https://ignis.venizia.ai/references/base/dependency-injection) &#8226;
180
+ [Core API](https://ignis.venizia.ai/references/) &#8226;
181
+ [Best practices](https://ignis.venizia.ai/best-practices/) &#8226;
182
+ [Changelog](https://ignis.venizia.ai/changelogs/)
2168
183
 
2169
- [MIT](./LICENSE.md)
184
+ MIT licensed. Questions: [GitHub Issues](https://github.com/VENIZIA-AI/ignis/issues) &#8226; developer@venizia.ai