@venizia/ignis-inversion 0.1.1-1 → 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.
- package/README.md +117 -2102
- package/dist/cjs/common/types.d.ts +2 -11
- package/dist/cjs/common/types.d.ts.map +1 -1
- package/dist/cjs/common/types.js +2 -11
- package/dist/cjs/common/types.js.map +1 -1
- package/dist/cjs/modules/container/abstract.d.ts +2 -4
- package/dist/cjs/modules/container/abstract.d.ts.map +1 -1
- package/dist/cjs/modules/container/abstract.js +2 -4
- package/dist/cjs/modules/container/abstract.js.map +1 -1
- package/dist/cjs/modules/container/base.d.ts +2 -4
- package/dist/cjs/modules/container/base.d.ts.map +1 -1
- package/dist/cjs/modules/container/base.js +2 -4
- package/dist/cjs/modules/container/base.js.map +1 -1
- package/dist/cjs/modules/container/container.js +1 -1
- package/dist/cjs/modules/container/container.js.map +1 -1
- package/dist/cjs/modules/error/types.d.ts +5 -12
- package/dist/cjs/modules/error/types.d.ts.map +1 -1
- package/dist/cjs/modules/metadata/common/constants.d.ts +0 -1
- package/dist/cjs/modules/metadata/common/constants.d.ts.map +1 -1
- package/dist/cjs/modules/metadata/common/constants.js +0 -1
- package/dist/cjs/modules/metadata/common/constants.js.map +1 -1
- package/dist/cjs/modules/metadata/injectors.d.ts +0 -2
- package/dist/cjs/modules/metadata/injectors.d.ts.map +1 -1
- package/dist/cjs/modules/metadata/injectors.js +1 -7
- package/dist/cjs/modules/metadata/injectors.js.map +1 -1
- package/dist/cjs/modules/registry/common/types.d.ts +0 -6
- package/dist/cjs/modules/registry/common/types.d.ts.map +1 -1
- package/dist/cjs/modules/registry/registry.d.ts +1 -8
- package/dist/cjs/modules/registry/registry.d.ts.map +1 -1
- package/dist/cjs/modules/registry/registry.js +0 -8
- package/dist/cjs/modules/registry/registry.js.map +1 -1
- package/dist/esm/common/types.d.ts +2 -11
- package/dist/esm/common/types.d.ts.map +1 -1
- package/dist/esm/common/types.js +2 -11
- package/dist/esm/common/types.js.map +1 -1
- package/dist/esm/modules/container/abstract.d.ts +2 -4
- package/dist/esm/modules/container/abstract.d.ts.map +1 -1
- package/dist/esm/modules/container/abstract.js +2 -4
- package/dist/esm/modules/container/abstract.js.map +1 -1
- package/dist/esm/modules/container/base.d.ts +2 -4
- package/dist/esm/modules/container/base.d.ts.map +1 -1
- package/dist/esm/modules/container/base.js +2 -4
- package/dist/esm/modules/container/base.js.map +1 -1
- package/dist/esm/modules/container/container.js +1 -1
- package/dist/esm/modules/container/container.js.map +1 -1
- package/dist/esm/modules/error/types.d.ts +5 -12
- package/dist/esm/modules/error/types.d.ts.map +1 -1
- package/dist/esm/modules/metadata/common/constants.d.ts +0 -1
- package/dist/esm/modules/metadata/common/constants.d.ts.map +1 -1
- package/dist/esm/modules/metadata/common/constants.js +0 -1
- package/dist/esm/modules/metadata/common/constants.js.map +1 -1
- package/dist/esm/modules/metadata/injectors.d.ts +0 -2
- package/dist/esm/modules/metadata/injectors.d.ts.map +1 -1
- package/dist/esm/modules/metadata/injectors.js +0 -5
- package/dist/esm/modules/metadata/injectors.js.map +1 -1
- package/dist/esm/modules/registry/common/types.d.ts +0 -6
- package/dist/esm/modules/registry/common/types.d.ts.map +1 -1
- package/dist/esm/modules/registry/registry.d.ts +1 -8
- package/dist/esm/modules/registry/registry.d.ts.map +1 -1
- package/dist/esm/modules/registry/registry.js +0 -8
- package/dist/esm/modules/registry/registry.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,2169 +1,184 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<br />
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# :fire: @venizia/ignis-inversion
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
+
**A small IoC container that works anywhere.**
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
[](https://ignis.venizia.ai/references/base/dependency-injection)
|
|
10
|
+
[](https://www.npmjs.com/package/@venizia/ignis-inversion)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://www.typescriptlang.org/)
|
|
12
13
|
|
|
13
|
-
[
|
|
14
|
+
[Documentation](https://ignis.venizia.ai/references/base/dependency-injection) •
|
|
15
|
+
[Core API](https://ignis.venizia.ai/references/) •
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
## Usage
|
|
165
42
|
|
|
166
43
|
```typescript
|
|
167
|
-
import
|
|
44
|
+
import 'reflect-metadata';
|
|
45
|
+
import { BindingScopes, Container, inject } from '@venizia/ignis-inversion';
|
|
168
46
|
|
|
169
|
-
|
|
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: '
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
return this.
|
|
53
|
+
greet(name: string) {
|
|
54
|
+
return `${this.prefix} ${name} (${this.locale ?? 'en'})`;
|
|
185
55
|
}
|
|
186
56
|
}
|
|
187
57
|
|
|
188
|
-
|
|
189
|
-
|
|
58
|
+
const container = new Container({ scope: 'app' });
|
|
59
|
+
|
|
60
|
+
container.bind({ key: 'config.prefix' }).toValue('Hello');
|
|
190
61
|
container
|
|
191
|
-
.bind({ key: '
|
|
192
|
-
.toClass(
|
|
193
|
-
.setScope(
|
|
62
|
+
.bind({ key: 'services.GreetingService' })
|
|
63
|
+
.toClass(GreetingService)
|
|
64
|
+
.setScope(BindingScopes.SINGLETON);
|
|
194
65
|
|
|
195
|
-
container.
|
|
66
|
+
const service = container.get<GreetingService>({ key: 'services.GreetingService' });
|
|
67
|
+
console.log(service.greet('IGNIS')); // Hello IGNIS (en)
|
|
196
68
|
|
|
197
|
-
//
|
|
198
|
-
|
|
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
|
-
|
|
75
|
+
Every method takes an options object - `container.get({ key })`, never `container.get(key)`.
|
|
205
76
|
|
|
206
77
|
### Container
|
|
207
78
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
## Errors
|
|
545
128
|
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
[Documentation](https://ignis.venizia.ai) •
|
|
179
|
+
[Dependency injection](https://ignis.venizia.ai/references/base/dependency-injection) •
|
|
180
|
+
[Core API](https://ignis.venizia.ai/references/) •
|
|
181
|
+
[Best practices](https://ignis.venizia.ai/best-practices/) •
|
|
182
|
+
[Changelog](https://ignis.venizia.ai/changelogs/)
|
|
2168
183
|
|
|
2169
|
-
[
|
|
184
|
+
MIT licensed. Questions: [GitHub Issues](https://github.com/VENIZIA-AI/ignis/issues) • developer@venizia.ai
|