@nexus-ioc/core 0.6.1 → 0.7.0
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 +731 -1
- package/dist/cjs/browser.js +2 -2
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/core/child-module.js +32 -0
- package/dist/cjs/core/child-module.js.map +1 -0
- package/dist/cjs/core/graph/analyze-lazy-module.js +54 -0
- package/dist/cjs/core/graph/analyze-lazy-module.js.map +1 -0
- package/dist/cjs/core/graph/analyze-module.js +11 -0
- package/dist/cjs/core/graph/analyze-module.js.map +1 -1
- package/dist/cjs/core/graph/module-graph.js +685 -70
- package/dist/cjs/core/graph/module-graph.js.map +1 -1
- package/dist/cjs/core/injection/injection-context.js +29 -0
- package/dist/cjs/core/injection/injection-context.js.map +1 -0
- package/dist/cjs/core/lazy-module-loader.js +41 -0
- package/dist/cjs/core/lazy-module-loader.js.map +1 -0
- package/dist/cjs/core/module-ref.js +84 -0
- package/dist/cjs/core/module-ref.js.map +1 -0
- package/dist/cjs/core/modules/container.js +305 -17
- package/dist/cjs/core/modules/container.js.map +1 -1
- package/dist/cjs/core/modules/module-container-factory.js +2 -2
- package/dist/cjs/core/modules/module-container-factory.js.map +1 -1
- package/dist/cjs/core/modules/module-container.js +54 -16
- package/dist/cjs/core/modules/module-container.js.map +1 -1
- package/dist/cjs/core/modules/module-token-factory.js.map +1 -1
- package/dist/cjs/core/modules/modules-container.js +43 -3
- package/dist/cjs/core/modules/modules-container.js.map +1 -1
- package/dist/cjs/core/nexus-applications.browser.js +57 -15
- package/dist/cjs/core/nexus-applications.browser.js.map +1 -1
- package/dist/cjs/core/nexus-applications.js +440 -82
- package/dist/cjs/core/nexus-applications.js.map +1 -1
- package/dist/cjs/core/nexus-applications.server.js +57 -15
- package/dist/cjs/core/nexus-applications.server.js.map +1 -1
- package/dist/cjs/core/resolver/providers-container.js.map +1 -1
- package/dist/cjs/core/resolver/resolution-context.js +62 -0
- package/dist/cjs/core/resolver/resolution-context.js.map +1 -0
- package/dist/cjs/core/resolver/resolver.js +523 -99
- package/dist/cjs/core/resolver/resolver.js.map +1 -1
- package/dist/cjs/decorators/inject.js +6 -3
- package/dist/cjs/decorators/inject.js.map +1 -1
- package/dist/cjs/decorators/injectable.js +0 -7
- package/dist/cjs/decorators/injectable.js.map +1 -1
- package/dist/cjs/decorators/{nsModule.js → module.js} +9 -7
- package/dist/cjs/decorators/module.js.map +1 -0
- package/dist/cjs/errors/bootstrap-error.js +47 -0
- package/dist/cjs/errors/bootstrap-error.js.map +1 -0
- package/dist/cjs/errors/child-errors.js +21 -0
- package/dist/cjs/errors/child-errors.js.map +1 -0
- package/dist/cjs/errors/index.js +24 -0
- package/dist/cjs/errors/index.js.map +1 -0
- package/dist/cjs/errors/injection-errors.js +25 -0
- package/dist/cjs/errors/injection-errors.js.map +1 -0
- package/dist/cjs/errors/lazy-module-errors.js +26 -0
- package/dist/cjs/errors/lazy-module-errors.js.map +1 -0
- package/dist/cjs/errors/provider-errors.js +95 -0
- package/dist/cjs/errors/provider-errors.js.map +1 -0
- package/dist/cjs/index.js +30 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interfaces/index.js +1 -0
- package/dist/cjs/interfaces/index.js.map +1 -1
- package/dist/cjs/interfaces/modules/index.js +1 -0
- package/dist/cjs/interfaces/modules/index.js.map +1 -1
- package/dist/cjs/interfaces/modules/module-graph.interface.js +2 -0
- package/dist/cjs/interfaces/modules/module-graph.interface.js.map +1 -1
- package/dist/cjs/interfaces/modules/parent-link.interface.js +3 -0
- package/dist/cjs/interfaces/modules/parent-link.interface.js.map +1 -0
- package/dist/cjs/interfaces/on-module-destroy.interface.js +3 -0
- package/dist/cjs/interfaces/on-module-destroy.interface.js.map +1 -0
- package/dist/cjs/interfaces/scope.interface.js +2 -55
- package/dist/cjs/interfaces/scope.interface.js.map +1 -1
- package/dist/cjs/interfaces/validate-module-keys.js +2 -0
- package/dist/cjs/interfaces/validate-module-keys.js.map +1 -1
- package/dist/cjs/internal.js +9 -0
- package/dist/cjs/internal.js.map +1 -0
- package/dist/cjs/server.js +2 -2
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/utils/forward-ref.js +14 -0
- package/dist/cjs/utils/forward-ref.js.map +1 -0
- package/dist/cjs/utils/helpers.js +14 -4
- package/dist/cjs/utils/helpers.js.map +1 -1
- package/dist/cjs/utils/lazy-module.js +33 -0
- package/dist/cjs/utils/lazy-module.js.map +1 -0
- package/dist/esm/browser.js +1 -1
- package/dist/esm/browser.js.map +1 -1
- package/dist/esm/core/child-module.js +29 -0
- package/dist/esm/core/child-module.js.map +1 -0
- package/dist/esm/core/graph/analyze-lazy-module.js +50 -0
- package/dist/esm/core/graph/analyze-lazy-module.js.map +1 -0
- package/dist/esm/core/graph/analyze-module.js +11 -0
- package/dist/esm/core/graph/analyze-module.js.map +1 -1
- package/dist/esm/core/graph/module-graph.js +686 -71
- package/dist/esm/core/graph/module-graph.js.map +1 -1
- package/dist/esm/core/injection/injection-context.js +25 -0
- package/dist/esm/core/injection/injection-context.js.map +1 -0
- package/dist/esm/core/lazy-module-loader.js +36 -0
- package/dist/esm/core/lazy-module-loader.js.map +1 -0
- package/dist/esm/core/module-ref.js +80 -0
- package/dist/esm/core/module-ref.js.map +1 -0
- package/dist/esm/core/modules/container.js +305 -17
- package/dist/esm/core/modules/container.js.map +1 -1
- package/dist/esm/core/modules/module-container-factory.js +2 -2
- package/dist/esm/core/modules/module-container-factory.js.map +1 -1
- package/dist/esm/core/modules/module-container.js +55 -17
- package/dist/esm/core/modules/module-container.js.map +1 -1
- package/dist/esm/core/modules/module-token-factory.js.map +1 -1
- package/dist/esm/core/modules/modules-container.js +43 -3
- package/dist/esm/core/modules/modules-container.js.map +1 -1
- package/dist/esm/core/nexus-applications.browser.js +55 -13
- package/dist/esm/core/nexus-applications.browser.js.map +1 -1
- package/dist/esm/core/nexus-applications.js +439 -81
- package/dist/esm/core/nexus-applications.js.map +1 -1
- package/dist/esm/core/nexus-applications.server.js +55 -13
- package/dist/esm/core/nexus-applications.server.js.map +1 -1
- package/dist/esm/core/resolver/providers-container.js.map +1 -1
- package/dist/esm/core/resolver/resolution-context.js +58 -0
- package/dist/esm/core/resolver/resolution-context.js.map +1 -0
- package/dist/esm/core/resolver/resolver.js +524 -100
- package/dist/esm/core/resolver/resolver.js.map +1 -1
- package/dist/esm/decorators/inject.js +6 -3
- package/dist/esm/decorators/inject.js.map +1 -1
- package/dist/esm/decorators/injectable.js +0 -7
- package/dist/esm/decorators/injectable.js.map +1 -1
- package/dist/esm/decorators/{nsModule.js → module.js} +8 -6
- package/dist/esm/decorators/module.js.map +1 -0
- package/dist/esm/errors/bootstrap-error.js +42 -0
- package/dist/esm/errors/bootstrap-error.js.map +1 -0
- package/dist/esm/errors/child-errors.js +17 -0
- package/dist/esm/errors/child-errors.js.map +1 -0
- package/dist/esm/errors/index.js +6 -0
- package/dist/esm/errors/index.js.map +1 -0
- package/dist/esm/errors/injection-errors.js +20 -0
- package/dist/esm/errors/injection-errors.js.map +1 -0
- package/dist/esm/errors/lazy-module-errors.js +21 -0
- package/dist/esm/errors/lazy-module-errors.js.map +1 -0
- package/dist/esm/errors/provider-errors.js +84 -0
- package/dist/esm/errors/provider-errors.js.map +1 -0
- package/dist/esm/index.js +9 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces/index.js +1 -0
- package/dist/esm/interfaces/index.js.map +1 -1
- package/dist/esm/interfaces/modules/index.js +1 -0
- package/dist/esm/interfaces/modules/index.js.map +1 -1
- package/dist/esm/interfaces/modules/module-graph.interface.js +2 -0
- package/dist/esm/interfaces/modules/module-graph.interface.js.map +1 -1
- package/dist/esm/interfaces/modules/parent-link.interface.js +2 -0
- package/dist/esm/interfaces/modules/parent-link.interface.js.map +1 -0
- package/dist/esm/interfaces/on-module-destroy.interface.js +2 -0
- package/dist/esm/interfaces/on-module-destroy.interface.js.map +1 -0
- package/dist/esm/interfaces/scope.interface.js +1 -55
- package/dist/esm/interfaces/scope.interface.js.map +1 -1
- package/dist/esm/interfaces/validate-module-keys.js +2 -0
- package/dist/esm/interfaces/validate-module-keys.js.map +1 -1
- package/dist/esm/internal.js +3 -0
- package/dist/esm/internal.js.map +1 -0
- package/dist/esm/server.js +1 -1
- package/dist/esm/server.js.map +1 -1
- package/dist/esm/utils/forward-ref.js +10 -0
- package/dist/esm/utils/forward-ref.js.map +1 -0
- package/dist/esm/utils/helpers.js +14 -4
- package/dist/esm/utils/helpers.js.map +1 -1
- package/dist/esm/utils/lazy-module.js +29 -0
- package/dist/esm/utils/lazy-module.js.map +1 -0
- package/dist/types/browser.d.ts +1 -1
- package/dist/types/core/child-module.d.ts +8 -0
- package/dist/types/core/graph/analyze-lazy-module.d.ts +28 -0
- package/dist/types/core/graph/analyze-module.d.ts +4 -2
- package/dist/types/core/graph/analyze-provider.d.ts +4 -4
- package/dist/types/core/graph/module-graph.d.ts +200 -7
- package/dist/types/core/graph/providers/analyze-class-provider.d.ts +1 -1
- package/dist/types/core/graph/providers/analyze-factory-provider.d.ts +2 -2
- package/dist/types/core/graph/providers/analyze-value-provider.d.ts +1 -1
- package/dist/types/core/injection/injection-context.d.ts +29 -0
- package/dist/types/core/lazy-module-loader.d.ts +17 -0
- package/dist/types/core/module-ref.d.ts +34 -0
- package/dist/types/core/modules/container.d.ts +134 -18
- package/dist/types/core/modules/module-container-factory.d.ts +3 -3
- package/dist/types/core/modules/module-container.d.ts +44 -5
- package/dist/types/core/modules/module-token-factory.d.ts +2 -2
- package/dist/types/core/modules/modules-container.d.ts +23 -4
- package/dist/types/core/nexus-applications.browser.d.ts +30 -8
- package/dist/types/core/nexus-applications.d.ts +219 -55
- package/dist/types/core/nexus-applications.server.d.ts +30 -8
- package/dist/types/core/resolver/providers-container.d.ts +2 -2
- package/dist/types/core/resolver/resolution-context.d.ts +22 -0
- package/dist/types/core/resolver/resolver.d.ts +122 -4
- package/dist/types/decorators/inject.d.ts +9 -5
- package/dist/types/decorators/injectable.d.ts +0 -7
- package/dist/types/decorators/{nsModule.d.ts → module.d.ts} +6 -5
- package/dist/types/errors/bootstrap-error.d.ts +6 -0
- package/dist/types/errors/child-errors.d.ts +10 -0
- package/dist/types/errors/index.d.ts +5 -0
- package/dist/types/errors/injection-errors.d.ts +11 -0
- package/dist/types/errors/lazy-module-errors.d.ts +12 -0
- package/dist/types/errors/provider-errors.d.ts +47 -0
- package/dist/types/index.d.ts +13 -4
- package/dist/types/interfaces/abstract.interface.d.ts +1 -3
- package/dist/types/interfaces/dynamic-module.interface.d.ts +7 -5
- package/dist/types/interfaces/index.d.ts +1 -0
- package/dist/types/interfaces/injection-token.interface.d.ts +1 -3
- package/dist/types/interfaces/module-metadata.interface.d.ts +12 -4
- package/dist/types/interfaces/module-types.interface.d.ts +21 -24
- package/dist/types/interfaces/modules/container.interface.d.ts +10 -4
- package/dist/types/interfaces/modules/index.d.ts +1 -0
- package/dist/types/interfaces/modules/module-container-factory.interface.d.ts +2 -2
- package/dist/types/interfaces/modules/module-graph.interface.d.ts +85 -12
- package/dist/types/interfaces/modules/module-token-factory.interface.d.ts +2 -2
- package/dist/types/interfaces/modules/modules-container.interface.d.ts +5 -4
- package/dist/types/interfaces/modules/parent-link.interface.d.ts +12 -0
- package/dist/types/interfaces/nexus-application.interface.d.ts +49 -6
- package/dist/types/interfaces/on-module-destroy.interface.d.ts +3 -0
- package/dist/types/interfaces/plugins/scanner-plugin.interface.d.ts +1 -1
- package/dist/types/interfaces/scope.interface.d.ts +1 -54
- package/dist/types/interfaces/validate-module-keys.d.ts +1 -0
- package/dist/types/internal.d.ts +3 -0
- package/dist/types/server.d.ts +1 -1
- package/dist/types/utils/forward-ref.d.ts +5 -0
- package/dist/types/utils/helpers.d.ts +8 -8
- package/dist/types/utils/lazy-module.d.ts +20 -0
- package/package.json +23 -7
- package/dist/cjs/decorators/nsModule.js.map +0 -1
- package/dist/esm/decorators/nsModule.js.map +0 -1
- package/dist/tsconfig.cjs.tsbuildinfo +0 -1
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -20,6 +20,13 @@ Nexus IoC is a powerful and flexible Inversion of Control (IoC) container for Ty
|
|
|
20
20
|
- [Features](#features)
|
|
21
21
|
- [Installation](#installation)
|
|
22
22
|
- [Quick Start](#quick-start)
|
|
23
|
+
- [Tokens](#tokens)
|
|
24
|
+
- [inject()](#inject)
|
|
25
|
+
- [get() vs resolve()](#get-vs-resolve)
|
|
26
|
+
- [Lifecycle and close()](#lifecycle-and-close)
|
|
27
|
+
- [Lazy Modules](#lazy-modules)
|
|
28
|
+
- [Child containers](#child-containers)
|
|
29
|
+
- [Migrating to the synchronous get()](#migrating-to-the-synchronous-get)
|
|
23
30
|
- [Testing](#testing)
|
|
24
31
|
- [License](#license)
|
|
25
32
|
- [Author](#author)
|
|
@@ -95,6 +102,673 @@ bootstrap();
|
|
|
95
102
|
|
|
96
103
|
```
|
|
97
104
|
|
|
105
|
+
`get()` is synchronous: singletons are already created when `bootstrap()`
|
|
106
|
+
resolves.
|
|
107
|
+
|
|
108
|
+
## Tokens
|
|
109
|
+
|
|
110
|
+
A provider is identified by a token: a class, an abstract class, a string, a
|
|
111
|
+
symbol or an `InjectionToken<T>`. The type that describes all of these is
|
|
112
|
+
`ProviderToken<T>`.
|
|
113
|
+
|
|
114
|
+
`InjectionToken<T>` is a typed token for values that have no class, such as
|
|
115
|
+
configuration:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Inject, Injectable, InjectionToken, Module } from '@nexus-ioc/core';
|
|
119
|
+
|
|
120
|
+
interface Config {
|
|
121
|
+
apiUrl: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const CONFIG = new InjectionToken<Config>('CONFIG');
|
|
125
|
+
|
|
126
|
+
@Injectable()
|
|
127
|
+
class ApiClient {
|
|
128
|
+
constructor(@Inject(CONFIG) private readonly config: Config) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Module({
|
|
132
|
+
providers: [
|
|
133
|
+
{ provide: CONFIG, useValue: { apiUrl: 'https://example.com' } },
|
|
134
|
+
ApiClient,
|
|
135
|
+
],
|
|
136
|
+
exports: [CONFIG],
|
|
137
|
+
})
|
|
138
|
+
class ApiModule {}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- Use it as `provide` with `useValue`, `useFactory` or `useClass`, with
|
|
142
|
+
`@Inject(TOKEN)` on constructor parameters and properties, with
|
|
143
|
+
`@Optional()`, and in module `exports`.
|
|
144
|
+
- Tokens are compared by identity. `description` is only a label, so two
|
|
145
|
+
`new InjectionToken('CONFIG')` instances are different tokens. Create the
|
|
146
|
+
token once and import the constant where it is needed.
|
|
147
|
+
- `token.toString()` returns `InjectionToken CONFIG`, and error messages use it.
|
|
148
|
+
|
|
149
|
+
`get()` and `resolve()` infer the result type from the token. This works for
|
|
150
|
+
`NexusApplication`, `ModuleRef`, `Test` and the object returned by
|
|
151
|
+
`Test.compile()`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const config = app.get(CONFIG); // Config
|
|
155
|
+
const api = app.get(ApiClient); // ApiClient
|
|
156
|
+
const raw = app.get('API_URL'); // unknown
|
|
157
|
+
const url = app.get<string>('API_URL'); // string
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Class tokens, including abstract classes, and `InjectionToken<T>` are
|
|
161
|
+
inferred. String and symbol tokens give `unknown` unless you pass `<T>` or the
|
|
162
|
+
expected type supplies it (`const url: string = app.get('API_URL')`).
|
|
163
|
+
`useValue` and `useFactory` results are not type-checked against the token's
|
|
164
|
+
`T`, so make sure the provided value matches it.
|
|
165
|
+
|
|
166
|
+
A class with a `private` or `protected` constructor cannot be used as a token,
|
|
167
|
+
because the container cannot construct it. Use an `InjectionToken` instead.
|
|
168
|
+
|
|
169
|
+
## inject()
|
|
170
|
+
|
|
171
|
+
`inject()` resolves a dependency from code instead of a constructor
|
|
172
|
+
parameter, in the style of Angular:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Inject, Injectable, InjectionToken, inject } from '@nexus-ioc/core';
|
|
176
|
+
|
|
177
|
+
const CONFIG = new InjectionToken<{ apiUrl: string }>('CONFIG');
|
|
178
|
+
|
|
179
|
+
@Injectable()
|
|
180
|
+
class Logger {}
|
|
181
|
+
|
|
182
|
+
// with @Inject
|
|
183
|
+
@Injectable()
|
|
184
|
+
class ApiClientA {
|
|
185
|
+
constructor(
|
|
186
|
+
private readonly logger: Logger,
|
|
187
|
+
@Inject(CONFIG) private readonly config: { apiUrl: string },
|
|
188
|
+
) {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// with inject()
|
|
192
|
+
@Injectable()
|
|
193
|
+
class ApiClientB {
|
|
194
|
+
private readonly logger = inject(Logger);
|
|
195
|
+
private readonly config = inject(CONFIG);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Signatures:
|
|
200
|
+
|
|
201
|
+
- `inject(token)` returns `T` and throws `ProviderNotFoundError` if the token is
|
|
202
|
+
not visible.
|
|
203
|
+
- `inject(token, { optional: true })` returns `T | undefined`.
|
|
204
|
+
- `inject(token, options: InjectOptions)` returns `T | undefined`.
|
|
205
|
+
|
|
206
|
+
### Where inject() works
|
|
207
|
+
|
|
208
|
+
Only synchronously, while the container is creating a provider:
|
|
209
|
+
|
|
210
|
+
- class field initializers;
|
|
211
|
+
- the constructor body and constructor parameter defaults;
|
|
212
|
+
- a `useFactory` function, before its first `await`;
|
|
213
|
+
- the same places in providers that are created as a consequence of an
|
|
214
|
+
`inject()` call.
|
|
215
|
+
|
|
216
|
+
Anywhere else `inject()` throws `InjectionContextError`: in `onModuleInit`
|
|
217
|
+
(including that of providers created through `inject()`), in methods, after an
|
|
218
|
+
`await`, and at module scope.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const FEATURES = new InjectionToken<string[]>('FEATURES');
|
|
222
|
+
|
|
223
|
+
@Module({
|
|
224
|
+
providers: [
|
|
225
|
+
Logger,
|
|
226
|
+
{
|
|
227
|
+
provide: FEATURES,
|
|
228
|
+
useFactory: () => {
|
|
229
|
+
const logger = inject(Logger); // ok: inside a factory function
|
|
230
|
+
return logger ? ['a', 'b'] : [];
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
})
|
|
235
|
+
class FeaturesModule {}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Visibility and optional
|
|
239
|
+
|
|
240
|
+
`inject()` follows the same rules as `@Inject`: the module of the requesting
|
|
241
|
+
provider must see the token (its own providers, exports of imported modules,
|
|
242
|
+
global modules). Otherwise `inject()` throws `ProviderNotFoundError`, whose
|
|
243
|
+
message names the requester (`requested by "Y" via inject()`; the error has a
|
|
244
|
+
`requestedBy` property). With `{ optional: true }` it returns `undefined`.
|
|
245
|
+
|
|
246
|
+
### Scopes
|
|
247
|
+
|
|
248
|
+
- Singleton: the cached instance, or a new one created synchronously on the
|
|
249
|
+
first `inject()`.
|
|
250
|
+
- Transient: a new instance on every `inject()` call.
|
|
251
|
+
|
|
252
|
+
### Async providers
|
|
253
|
+
|
|
254
|
+
A `useFactory` must return its value synchronously. A factory that returns a
|
|
255
|
+
promise (any thenable) throws `AsyncFactoryError`, which names the token, when
|
|
256
|
+
the provider is created: at `bootstrap()`, on `resolve()` in lazy mode, or from
|
|
257
|
+
`inject()`. A returned promise never becomes an unhandled rejection. Move async
|
|
258
|
+
setup into an `onModuleInit` (of the provided class, or of the object the
|
|
259
|
+
factory returns), or create the value before bootstrap and register it with
|
|
260
|
+
`useValue`.
|
|
261
|
+
|
|
262
|
+
An `onModuleInit` of an injected dependency may be async: it completes before
|
|
263
|
+
the requester's own `onModuleInit` runs. A singleton whose async init fails is
|
|
264
|
+
not left cached, and neither is a provider that holds it; the wrapped error has
|
|
265
|
+
the original as `.cause`. When a singleton was created by `inject()`, another
|
|
266
|
+
resolution (for example a concurrent `resolve()`) that meets it while its init
|
|
267
|
+
is still running waits for that init.
|
|
268
|
+
|
|
269
|
+
Concurrent `resolve()` calls that reach the same singleton before it exists
|
|
270
|
+
(directly or through `@Inject`, for example in `.lazy()` mode) create it once:
|
|
271
|
+
the later calls wait for the first creation, including its `onModuleInit`. If
|
|
272
|
+
that creation fails, every waiting call rejects with the same error and a later
|
|
273
|
+
`resolve()` tries again. `inject()` is synchronous and cannot wait, so an
|
|
274
|
+
`inject()` that meets a singleton another `resolve()` is still creating throws
|
|
275
|
+
`ProviderNotInstantiatedError`; await that `resolve()` first.
|
|
276
|
+
|
|
277
|
+
A dependency obtained with `inject()` may still be completing its async
|
|
278
|
+
`onModuleInit` while your constructor or factory body runs; it is guaranteed to
|
|
279
|
+
be done only before your own `onModuleInit`. For a dependency you must use in
|
|
280
|
+
the constructor or factory body after its async init, use `@Inject` (or the
|
|
281
|
+
factory's `inject: [...]`): when the requester is created by `bootstrap()` or
|
|
282
|
+
`resolve()`, that resolves the dependency fully first. When the requester is
|
|
283
|
+
itself created through `inject()`, its `@Inject` dependencies get the same
|
|
284
|
+
guarantee as `inject()` ones: done only before its own `onModuleInit`. Until
|
|
285
|
+
that init settles, `app.get()` of such a singleton throws
|
|
286
|
+
`ProviderNotInstantiatedError`, just as for a singleton still being created by
|
|
287
|
+
`resolve()`. Don't resolve providers that depend on a provider from that
|
|
288
|
+
provider's own `onModuleInit` (for example `await app.resolve(A)` in the
|
|
289
|
+
`onModuleInit` of `S`, where `A` injects `S`): the resolution waits for the
|
|
290
|
+
init that is waiting for it, and never settles. This is not detected, because
|
|
291
|
+
the inner `resolve()` cannot be told apart from an unrelated concurrent one.
|
|
292
|
+
|
|
293
|
+
Under concurrency a `forwardRef` dependency may be the proxy rather than the
|
|
294
|
+
real instance.
|
|
295
|
+
|
|
296
|
+
### Cycles
|
|
297
|
+
|
|
298
|
+
Any cycle that goes through `inject()` throws `CircularInjectionError` with the
|
|
299
|
+
path, for example `A -> B -> A`.
|
|
300
|
+
|
|
301
|
+
### Validation and tooling
|
|
302
|
+
|
|
303
|
+
`inject()` dependencies are found at runtime, so compile-time graph validation
|
|
304
|
+
does not see them and errors surface when the provider is created (for
|
|
305
|
+
singletons, in `bootstrap()`). They are recorded as runtime edges
|
|
306
|
+
(`inject: "runtime"`) in the module graph, so unloading a lazy module is
|
|
307
|
+
protected against modules used via `inject()` the same way as for `@Inject`.
|
|
308
|
+
One consequence: a provider of an always-loaded module (for example the root
|
|
309
|
+
module) that `inject()`s from a loaded lazy `@Global()` module keeps that
|
|
310
|
+
module loaded, so `unload()` of it becomes a no-op. Static `@Optional()`
|
|
311
|
+
dependencies follow the same visibility rule (`undefined` when the provider is
|
|
312
|
+
invisible) and pin the module of a later-loaded provider the same way
|
|
313
|
+
`inject()` does.
|
|
314
|
+
|
|
315
|
+
The static type-checker and graph-analyzer do see `inject()` calls, including
|
|
316
|
+
aliased imports. They report them with `location: "inject"` and include them
|
|
317
|
+
in graphs and cycle detection. They do not see namespace calls
|
|
318
|
+
(`core.inject()`) or factories declared in another place.
|
|
319
|
+
|
|
320
|
+
### Testing
|
|
321
|
+
|
|
322
|
+
`inject()` works with `Test.createModule(...).compile()`; see
|
|
323
|
+
[@nexus-ioc/testing](../testing/README.md).
|
|
324
|
+
|
|
325
|
+
## get() vs resolve()
|
|
326
|
+
|
|
327
|
+
- `app.get(token)` is synchronous and returns an already created singleton.
|
|
328
|
+
- `await app.resolve(token)` creates the instance on demand. Use it for
|
|
329
|
+
`Transient` providers and for providers in lazy mode.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const singleton = app.get<AppService>(AppService);
|
|
333
|
+
const transient = await app.resolve<TransientService>(TransientService);
|
|
334
|
+
|
|
335
|
+
// Pass { optional: true } to get undefined instead of an error for an
|
|
336
|
+
// unknown token.
|
|
337
|
+
const maybe = app.get<AppService>(AppService, { optional: true });
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`get()` throws:
|
|
341
|
+
|
|
342
|
+
- `ProviderNotFoundError` — the token is not a provider in this container
|
|
343
|
+
(or what its parent link provides). With `{ optional: true }` it returns
|
|
344
|
+
`undefined` instead.
|
|
345
|
+
- `ProviderScopeError` — the provider is `Transient`; use
|
|
346
|
+
`resolve()`.
|
|
347
|
+
- `ProviderNotInstantiatedError` — the singleton has not been created yet
|
|
348
|
+
(`.lazy()` mode, or a lazy module that is not loaded); use `resolve()` or
|
|
349
|
+
`load()` first.
|
|
350
|
+
- `ContainerClosedError` — the container is closed or closing.
|
|
351
|
+
- `ContainerNotCompiledError` — `bootstrap()` has not run yet. The parent is
|
|
352
|
+
not consulted in this case.
|
|
353
|
+
|
|
354
|
+
A token found in this container is always answered here; only unknown tokens
|
|
355
|
+
are looked up in the parent (for a child made with `createChild()`, through its link). Falsy
|
|
356
|
+
`useValue` values (`undefined`, `null`, `0`, `false`) are returned as they are.
|
|
357
|
+
|
|
358
|
+
`bootstrap()` rejects with `InvalidScopeError` for a provider whose `scope` is
|
|
359
|
+
not a `Scope` member (`Singleton` or `Transient`).
|
|
360
|
+
|
|
361
|
+
`resolve()` takes the same `{ optional }` option. It throws
|
|
362
|
+
`ProviderNotFoundError`, `ContainerClosedError` and
|
|
363
|
+
`ContainerNotCompiledError` the same way.
|
|
364
|
+
|
|
365
|
+
## Lifecycle and close()
|
|
366
|
+
|
|
367
|
+
`onModuleInit` runs during `bootstrap()`. `await app.close()` runs
|
|
368
|
+
`onModuleDestroy` on every provider in reverse creation order:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
@Injectable()
|
|
372
|
+
class Connection implements OnModuleDestroy {
|
|
373
|
+
async onModuleDestroy() {
|
|
374
|
+
await this.socket.end();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await app.close();
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- A hook that throws does not stop the others. After all of them have run,
|
|
382
|
+
`close()` throws one `AggregateError` with every failure.
|
|
383
|
+
- `close()` is idempotent: it returns the same promise on every call, and a
|
|
384
|
+
failed close keeps returning the same rejected promise.
|
|
385
|
+
- It first waits for in-flight lazy module loads. A lazy loader that never
|
|
386
|
+
settles makes `close()` hang.
|
|
387
|
+
- From the moment `close()` is called (before the in-flight loads have
|
|
388
|
+
drained), `get`, `resolve`, `load` and `unload` throw
|
|
389
|
+
`ContainerClosedError`. A load that was already in flight still finishes,
|
|
390
|
+
and its singletons are destroyed with the rest.
|
|
391
|
+
- A `ModuleRef` follows its own visibility rules first: for a token the
|
|
392
|
+
module can see, `ref.get()`/`ref.resolve()` throw `ContainerClosedError`
|
|
393
|
+
after `close()`; for a token it cannot see (or after `unload()`), they
|
|
394
|
+
keep returning `undefined`. `ref.unload()` after `close()` rejects with
|
|
395
|
+
`ContainerClosedError`.
|
|
396
|
+
- `close()` first closes the live child containers (see
|
|
397
|
+
[Child containers](#child-containers)), newest first; their errors join the
|
|
398
|
+
`AggregateError`. A service that subscribed to a parent service must
|
|
399
|
+
unsubscribe in `onModuleDestroy`.
|
|
400
|
+
- A `resolve()` that is still in flight when `close()` is called is not
|
|
401
|
+
awaited, so await your `resolve()` calls before closing.
|
|
402
|
+
- A singleton that such a `resolve()` is still creating when `close()` runs,
|
|
403
|
+
or when an unload destroys its module, is not cached. Its `onModuleDestroy`
|
|
404
|
+
is called on a best-effort basis, and the `resolve()` rejects with
|
|
405
|
+
`ContainerClosedError` (after `close()`) or `ProviderUnloadedError` (after
|
|
406
|
+
an unload).
|
|
407
|
+
|
|
408
|
+
## Lazy Modules
|
|
409
|
+
|
|
410
|
+
Declare a module import as lazy to keep it out of the initial bundle. The
|
|
411
|
+
module is loaded into the same container on demand and its providers become
|
|
412
|
+
resolvable afterwards.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { lazy, Module, NexusApplication } from '@nexus-ioc/core';
|
|
416
|
+
|
|
417
|
+
export const FeatureLazy = lazy(() =>
|
|
418
|
+
import('./feature/feature.module').then((m) => m.FeatureModule),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
@Module({ imports: [CoreModule, FeatureLazy] })
|
|
422
|
+
export class AppModule {}
|
|
423
|
+
|
|
424
|
+
const app = await NexusApplication.create(AppModule).bootstrap();
|
|
425
|
+
const ref = await app.load(FeatureLazy);
|
|
426
|
+
const service = ref.get<FeatureService>(FeatureService);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`lazy()` also takes a `name`, used in diagnostics, in error messages and, by
|
|
430
|
+
the Vite plugin, for chunk naming. Without it every ref is called
|
|
431
|
+
`"LazyModule"`, so name the ones you want to recognize:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
export const FeatureLazy = lazy(
|
|
435
|
+
() => import('./feature/feature.module').then((m) => m.FeatureModule),
|
|
436
|
+
{ name: 'Feature' },
|
|
437
|
+
);
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Rules:
|
|
441
|
+
|
|
442
|
+
- Eager providers cannot depend on providers of a not yet loaded module;
|
|
443
|
+
`bootstrap()` reports it as a missing provider.
|
|
444
|
+
- Loading is idempotent: the loader runs once per `lazy()` ref.
|
|
445
|
+
- Modules already in the container (for example a `SharedModule` imported by
|
|
446
|
+
both the root and the lazy module) are reused, so singletons are shared.
|
|
447
|
+
- A lazy module that registers a token another module already provides fails
|
|
448
|
+
with `PROVIDER_TOKEN_CONFLICT` and is rolled back.
|
|
449
|
+
|
|
450
|
+
### ModuleRef.get() and ModuleRef.resolve()
|
|
451
|
+
|
|
452
|
+
`ModuleRef.get()` is synchronous and `ModuleRef.resolve()` is async, with the
|
|
453
|
+
same split as on the application: `get()` for singletons, `resolve()` for
|
|
454
|
+
`Transient` and lazy-mode providers. Both are strict by default: they see the module's own providers and
|
|
455
|
+
what its imports (and global modules) export to them, and return `undefined`
|
|
456
|
+
for every other token — including tokens that do exist elsewhere in the
|
|
457
|
+
container. Pass `{ strict: false }` to look the token up in the whole
|
|
458
|
+
container instead. Either way the result is `T | undefined`, so check it
|
|
459
|
+
(`resolve()` returns `Promise<T | undefined>`):
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
const own = ref.get<FeatureService>(FeatureService); // visible: instance
|
|
463
|
+
const other = ref.get<UnrelatedService>(UnrelatedService); // undefined
|
|
464
|
+
const anywhere = ref.get<UnrelatedService>(UnrelatedService, {
|
|
465
|
+
strict: false,
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Errors
|
|
470
|
+
|
|
471
|
+
- `LazyModuleLoadError` — the loader rejected (a failed dynamic import, for
|
|
472
|
+
example) or did not return a class decorated with `@Module()`. Nothing is
|
|
473
|
+
added to the graph and the ref may be loaded again.
|
|
474
|
+
- `LazyModuleGraphError` — the module loaded but its dependency graph is
|
|
475
|
+
invalid (a missing dependency, a cycle, a token conflict). Its `errors`
|
|
476
|
+
property carries the graph errors, and the segment is rolled back entirely,
|
|
477
|
+
so the container is left exactly as it was.
|
|
478
|
+
|
|
479
|
+
### Loading from a service
|
|
480
|
+
|
|
481
|
+
To load from a service, inject the built-in `LazyModuleLoader`:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
@Injectable()
|
|
485
|
+
class OrdersService {
|
|
486
|
+
constructor(@Inject(LazyModuleLoader) private readonly loader: LazyModuleLoader) {}
|
|
487
|
+
|
|
488
|
+
async check(order: Order) {
|
|
489
|
+
const ref = await this.loader.load(FraudLazy);
|
|
490
|
+
const fraud = ref.get<FraudService>(FraudService);
|
|
491
|
+
return fraud?.check(order);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Under `@nexus-ioc/testing` the same loader is registered by `Test.compile()`,
|
|
497
|
+
so services that inject it work in tests; the `Test` instance itself also has
|
|
498
|
+
`load(ref)` once it is compiled.
|
|
499
|
+
|
|
500
|
+
### Unloading
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const ref = await app.load(FeatureLazy);
|
|
504
|
+
// ... later ...
|
|
505
|
+
const { destroyedModules, destroyedProviders, closedChildren } =
|
|
506
|
+
await app.unload(ref);
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
- `app.unload(ref)`, `ref.unload()`, and `LazyModuleLoader.unload(ref)` all do the
|
|
510
|
+
same thing; use whichever is convenient at the call site.
|
|
511
|
+
- Only what nothing else still needs is destroyed: a module shared with
|
|
512
|
+
another loaded segment, a `@Global()` module another segment depends on, or
|
|
513
|
+
a nested lazy module loaded from inside the one being unloaded, are all left
|
|
514
|
+
running.
|
|
515
|
+
- `onModuleDestroy` runs on affected singletons in reverse initialization
|
|
516
|
+
order. A hook that throws does not stop the others, and the module is
|
|
517
|
+
unloaded anyway (it can be loaded again); the unload then rejects with one
|
|
518
|
+
`AggregateError` holding every failure, as `close()` does. `ref.loaded` becomes `false`, and `ref.get()` and `ref.resolve()` return
|
|
519
|
+
`undefined` for every token afterward — the ref does not come back to life; loading the
|
|
520
|
+
same `lazy()` ref again returns a new `ModuleRef`.
|
|
521
|
+
- Unloading a ref that was never loaded, or unloading it twice, is a no-op:
|
|
522
|
+
`{ destroyedModules: [], destroyedProviders: [], closedChildren: 0 }`. After `app.close()`,
|
|
523
|
+
unloading rejects with `ContainerClosedError`.
|
|
524
|
+
- Child containers declared by the modules the unload destroys are closed
|
|
525
|
+
first (`closedChildren` counts them); see [Child containers](#child-containers).
|
|
526
|
+
- Not handled: an instance obtained via `app.get()` or `ref.get()` and held
|
|
527
|
+
outside the container (for example in a closure) keeps working as a plain
|
|
528
|
+
object after its module is destroyed — the same contract as an Angular
|
|
529
|
+
injector destroyed while a component still holds an already-injected
|
|
530
|
+
instance. A `get()` call racing an unload that destroys the same provider
|
|
531
|
+
mid-resolution is not guarded against. Likewise, calling `load()` on a ref
|
|
532
|
+
while an `unload()` of that same ref is in flight, without awaiting the
|
|
533
|
+
unload first, can return a `ModuleRef` for a segment that is about to be
|
|
534
|
+
destroyed — this race is not guarded against either.
|
|
535
|
+
|
|
536
|
+
## Child containers
|
|
537
|
+
|
|
538
|
+
A child container is a second application created from a module of a running
|
|
539
|
+
one. It has its own singletons and its own lifetime, and it can use the
|
|
540
|
+
providers of the application that created it. Typical uses: per-profile,
|
|
541
|
+
per-tenant or per-document state that is created and thrown away while the main
|
|
542
|
+
application keeps running.
|
|
543
|
+
|
|
544
|
+
### Per-profile containers
|
|
545
|
+
|
|
546
|
+
Declare the module that may become a child in `children` of the module whose
|
|
547
|
+
providers it should use, bootstrap the application, then call `createChild()`:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import {
|
|
551
|
+
DynamicModule,
|
|
552
|
+
Inject,
|
|
553
|
+
Injectable,
|
|
554
|
+
InjectionToken,
|
|
555
|
+
Module,
|
|
556
|
+
NexusApplication,
|
|
557
|
+
} from '@nexus-ioc/core';
|
|
558
|
+
|
|
559
|
+
const PROFILE_ID = new InjectionToken<string>('PROFILE_ID');
|
|
560
|
+
|
|
561
|
+
@Injectable()
|
|
562
|
+
class HttpClient {}
|
|
563
|
+
|
|
564
|
+
@Module({ providers: [HttpClient], exports: [HttpClient] })
|
|
565
|
+
class HttpModule {}
|
|
566
|
+
|
|
567
|
+
@Injectable()
|
|
568
|
+
class EventBus {}
|
|
569
|
+
|
|
570
|
+
@Injectable()
|
|
571
|
+
class ProfileStore {
|
|
572
|
+
constructor(
|
|
573
|
+
@Inject(PROFILE_ID) readonly profileId: string,
|
|
574
|
+
@Inject(EventBus) readonly bus: EventBus,
|
|
575
|
+
@Inject(HttpClient) readonly http: HttpClient,
|
|
576
|
+
) {}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
@Module({ providers: [ProfileStore] })
|
|
580
|
+
class ProfileModule {
|
|
581
|
+
static forProfile(id: string): DynamicModule {
|
|
582
|
+
return {
|
|
583
|
+
module: ProfileModule,
|
|
584
|
+
providers: [ProfileStore, { provide: PROFILE_ID, useValue: id }],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@Module({ imports: [HttpModule], providers: [EventBus], children: [ProfileModule] })
|
|
590
|
+
class AppModule {}
|
|
591
|
+
|
|
592
|
+
const app = await NexusApplication.create(AppModule).bootstrap();
|
|
593
|
+
let profile = await app.createChild(ProfileModule.forProfile('a'));
|
|
594
|
+
profile.get(ProfileStore).profileId; // 'a'
|
|
595
|
+
// switch profile
|
|
596
|
+
await profile.close();
|
|
597
|
+
profile = await app.createChild(ProfileModule.forProfile('b'));
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
`createChild()` is a method of the application, not something a provider can
|
|
601
|
+
inject: there is no DI handle for it yet. Keep the `app` at the composition
|
|
602
|
+
root and pass it (or a function that calls `app.createChild()`) to whatever
|
|
603
|
+
switches profiles.
|
|
604
|
+
|
|
605
|
+
`createChild()` returns a `NexusApplication` that is already bootstrapped, with
|
|
606
|
+
its singletons created. As with any dynamic module, the `providers` of the
|
|
607
|
+
`DynamicModule` object are used instead of the ones in the class's `@Module`
|
|
608
|
+
decorator, which is why `forProfile()` lists `ProfileStore` too. `ProfileStore` gets `EventBus` and `HttpClient` from
|
|
609
|
+
`AppModule`; the profile id is data that exists only per child, so it is
|
|
610
|
+
supplied when the child is created.
|
|
611
|
+
|
|
612
|
+
### Declaring children
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
const ReportsLazy = lazy(() =>
|
|
616
|
+
import('./reports/reports.module').then((m) => m.ReportsModule),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
@Module({
|
|
620
|
+
children: [ProfileModule, ReportsLazy],
|
|
621
|
+
})
|
|
622
|
+
class AppModule {}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- `children` takes classes and `lazy()` refs. It is static `@Module` metadata:
|
|
626
|
+
a `DynamicModule` object cannot declare children.
|
|
627
|
+
- A child module is not an import. It is not compiled into the parent graph and
|
|
628
|
+
nothing is created until you call `createChild()`.
|
|
629
|
+
- A given child may be declared by at most one module in the whole parent
|
|
630
|
+
graph. A second declaration is the graph error `CHILD_DECLARED_TWICE`. It is
|
|
631
|
+
also raised when one class is imported as two module ids (two `forX()`
|
|
632
|
+
variants, or a static plus a dynamic import) in a graph where that class
|
|
633
|
+
declares children.
|
|
634
|
+
- A class (`Type`) child and a `lazy()` child that load the same class are
|
|
635
|
+
different declarations.
|
|
636
|
+
- A `lazy()` ref is matched by identity.
|
|
637
|
+
|
|
638
|
+
### createChild()
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
createChild(
|
|
642
|
+
child: Type | DynamicModule | LazyModule,
|
|
643
|
+
options?: { providers?: Provider[] },
|
|
644
|
+
): Promise<NexusApplication>
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
- A class matches a declared class. A `DynamicModule` matches by its `.module`
|
|
648
|
+
class, so `ProfileModule.forProfile(id)` works when `ProfileModule` is
|
|
649
|
+
declared. A `lazy()` ref matches the declared ref.
|
|
650
|
+
- The loader of a lazy child runs once per ref per parent and its result is
|
|
651
|
+
cached; a failed load is not cached. The loaded class must be the declared
|
|
652
|
+
one or carry `@Module`; otherwise `LazyModuleLoadError`.
|
|
653
|
+
- `options.providers` are appended to the providers of the child's root
|
|
654
|
+
module. Use them for per-instance data:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
const profile = await app.createChild(ProfileModule, {
|
|
658
|
+
providers: [{ provide: PROFILE_ID, useValue: 'a' }],
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
- A child root created with `forProfile(id)` or `options.providers` keeps the
|
|
662
|
+
`children` its class declares, so a child can have children of its own.
|
|
663
|
+
|
|
664
|
+
`createChild()` rejects with:
|
|
665
|
+
|
|
666
|
+
- `ChildModuleNotDeclaredError` — no module declares the child. Its `reason`
|
|
667
|
+
is `"unloaded"` when the lazy module that declares it was unloaded while the
|
|
668
|
+
child was being created (a child that had already been started is closed).
|
|
669
|
+
- `ContainerNotCompiledError` — the parent is not bootstrapped.
|
|
670
|
+
- `ContainerClosedError` — the parent is closed or closing. It wins when the
|
|
671
|
+
parent closes during creation.
|
|
672
|
+
- `LazyModuleLoadError` — the loader of a lazy child failed or returned
|
|
673
|
+
something that is not a module.
|
|
674
|
+
|
|
675
|
+
### What a child sees
|
|
676
|
+
|
|
677
|
+
A child sees what its declaring module sees in the parent graph: that module's
|
|
678
|
+
own providers, what its imports export to it and global modules, plus whatever
|
|
679
|
+
the parent's own link provides, recursively up the chain. Providers a module
|
|
680
|
+
does not export stay hidden. A provider visible in the child itself wins over
|
|
681
|
+
the parent's.
|
|
682
|
+
|
|
683
|
+
- `child.get(token)` and `child.resolve(token)` look in the child first (any
|
|
684
|
+
provider of the child, whatever its exports), then through the link, then
|
|
685
|
+
throw `ProviderNotFoundError` (or return `undefined` with
|
|
686
|
+
`{ optional: true }`).
|
|
687
|
+
- After the parent closes, lookups through the link throw
|
|
688
|
+
`ContainerClosedError`, even with `{ optional: true }`.
|
|
689
|
+
- A child that imports a module itself gets its own instances of that
|
|
690
|
+
module's providers. If `ProfileModule` imports `HttpModule`, every profile
|
|
691
|
+
has a second `HttpClient` singleton, separate from the parent's. To share the
|
|
692
|
+
parent's instance, do not import the module in the child; let the token come
|
|
693
|
+
through the declaring module's visibility (here, `AppModule` imports
|
|
694
|
+
`HttpModule`).
|
|
695
|
+
- Scanner plugins added with `addScannerPlugin()` are not inherited: they run
|
|
696
|
+
on the parent's graph only, never on a child's.
|
|
697
|
+
|
|
698
|
+
### Scopes across the boundary
|
|
699
|
+
|
|
700
|
+
- `@Inject` of a parent token gives the parent's singleton instance. For a
|
|
701
|
+
parent `Transient` it gives a new instance, created by the parent.
|
|
702
|
+
- `inject()` uses the link's synchronous `get`, which cannot create anything:
|
|
703
|
+
- a parent `Transient` reached through `inject()` throws
|
|
704
|
+
`ProviderScopeError`;
|
|
705
|
+
- a parent singleton that does not exist yet throws
|
|
706
|
+
`ProviderNotInstantiatedError`. That is the case for a parent in `.lazy()`
|
|
707
|
+
mode, for a parent singleton whose async `onModuleInit` is still running,
|
|
708
|
+
and while a `load()` warm-up is still creating it.
|
|
709
|
+
|
|
710
|
+
Use `@Inject` for dependencies on the parent when any of those can happen.
|
|
711
|
+
- A required dependency that neither the child nor the link provides fails the
|
|
712
|
+
child's `bootstrap()`, so `createChild()` rejects with `BootstrapError`
|
|
713
|
+
instead of creating a provider with an `undefined` dependency.
|
|
714
|
+
|
|
715
|
+
### Lifecycle
|
|
716
|
+
|
|
717
|
+
- The parent tracks its live children. `child.close()` closes only that child
|
|
718
|
+
and removes it from the parent, so it can be garbage-collected; closing a
|
|
719
|
+
child does not touch the parent or its siblings.
|
|
720
|
+
- `parent.close()` first closes all live children, newest first, then tears
|
|
721
|
+
itself down. Errors from the children join the parent's `AggregateError`.
|
|
722
|
+
- Unloading a lazy module closes every live child declared by a module the
|
|
723
|
+
unload destroys, before it destroys providers. `UnloadResult` has a
|
|
724
|
+
`closedChildren` count of the closes that unload itself started (a child that
|
|
725
|
+
was already closing is not counted). A child-close error and a
|
|
726
|
+
provider-destroy error during one unload are joined into one `AggregateError`.
|
|
727
|
+
- A singleton creation that completes after `close()` or an unload is not
|
|
728
|
+
cached, in a child as in any application; see
|
|
729
|
+
[Lifecycle and close()](#lifecycle-and-close).
|
|
730
|
+
- A child's `onModuleDestroy` that awaits `parent.load()` or `parent.unload()`
|
|
731
|
+
while that parent is unloading hangs, because the parent's compile queue is
|
|
732
|
+
busy. After an unload that is not followed by a reload, a stalled async tree
|
|
733
|
+
may construct a requester with an `undefined` dependency before it is
|
|
734
|
+
discarded.
|
|
735
|
+
|
|
736
|
+
### Concurrent children
|
|
737
|
+
|
|
738
|
+
Children of the same module are independent containers with separate
|
|
739
|
+
singletons. Creating several at once is fine:
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const [a, b] = await Promise.all([
|
|
743
|
+
app.createChild(ProfileModule.forProfile('a')),
|
|
744
|
+
app.createChild(ProfileModule.forProfile('b')),
|
|
745
|
+
]);
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### React and other UI scopes
|
|
749
|
+
|
|
750
|
+
A React `<ContainerScope>` maps onto this directly: call `createChild()` in an
|
|
751
|
+
effect and `close()` in its cleanup. In StrictMode effects run twice in
|
|
752
|
+
development, so guard against the double mount: close the first child in the
|
|
753
|
+
cleanup, and close a child whose creation finished after its effect was already
|
|
754
|
+
cleaned up. The adapter itself is not part of this package.
|
|
755
|
+
|
|
756
|
+
### Static analysis
|
|
757
|
+
|
|
758
|
+
`@nexus-ioc/type-checker` and `@nexus-ioc/graph-analyzer` follow `children`
|
|
759
|
+
(direct class references and the
|
|
760
|
+
`lazy(() => import('...').then((m) => m.X))` shape). The type-checker resolves
|
|
761
|
+
each child scope's dependencies with the declaring module's visibility as the
|
|
762
|
+
fallback, and the graph-analyzer reports child scopes. The language service
|
|
763
|
+
suppresses missing-dependency errors in a child scope when the declaring module
|
|
764
|
+
can see the token. See [graph-analyzer](../graph-analyzer/README.md).
|
|
765
|
+
|
|
766
|
+
Known limitation: static tooling only sees what the source declares. Tokens a
|
|
767
|
+
child gets per instance (the `providers` of `forProfile()` or another dynamic
|
|
768
|
+
module, or `createChild(..., { providers })`) are invisible to it, so the
|
|
769
|
+
language service and the type-checker may report them as missing even though
|
|
770
|
+
the application runs.
|
|
771
|
+
|
|
98
772
|
## Testing
|
|
99
773
|
|
|
100
774
|
### Installation
|
|
@@ -118,12 +792,68 @@ describe('AppModule', () => {
|
|
|
118
792
|
providers: [AppService],
|
|
119
793
|
}).compile();
|
|
120
794
|
|
|
121
|
-
const appService =
|
|
795
|
+
const appService = appModule.get<AppService>(AppService);
|
|
122
796
|
expect(appService).toBeInstanceOf(AppService);
|
|
123
797
|
});
|
|
124
798
|
});
|
|
125
799
|
```
|
|
126
800
|
|
|
801
|
+
`Test.compile()` instantiates all singletons and runs `onModuleInit`, so an
|
|
802
|
+
error in `onModuleInit` or in a factory surfaces from `compile()`. `Test.get`
|
|
803
|
+
is synchronous and `Test.resolve` is async, both with `{ optional }`. The
|
|
804
|
+
module container that `compile()` returns has the same `get()` / `resolve()`
|
|
805
|
+
pair, so `moduleRef.get<X>(X)` needs no `await`.
|
|
806
|
+
`Test.load()` warms the loaded module.
|
|
807
|
+
|
|
808
|
+
## Migrating to the synchronous get()
|
|
809
|
+
|
|
810
|
+
- The `InjectionToken` type is now `ProviderToken<T>`, and `InjectionToken` is
|
|
811
|
+
a class (see [Tokens](#tokens)). There is no alias for the old type, so
|
|
812
|
+
replace type-level uses of `InjectionToken` with `ProviderToken`.
|
|
813
|
+
- `get('string')` and `get(symbol)` now return `unknown` unless you pass `<T>`
|
|
814
|
+
(`get<string>('API_URL')`) or the expected type supplies it. Class tokens and
|
|
815
|
+
`InjectionToken<T>` are inferred.
|
|
816
|
+
- `ProviderToken<T>` no longer accepts arbitrary functions: a plain function,
|
|
817
|
+
or a class unrelated to `T` (`get<Foo>(Bar)`), is now a compile error.
|
|
818
|
+
- A class with a `private` or `protected` constructor can no longer be used as
|
|
819
|
+
a token: use an `InjectionToken` instead.
|
|
820
|
+
- Async factories are removed: a `useFactory` that returns a promise throws
|
|
821
|
+
`AsyncFactoryError`. Move the async setup into `onModuleInit` of the provided
|
|
822
|
+
class, or create the value before bootstrap and register it with `useValue`.
|
|
823
|
+
Singletons are now created in plain declaration order.
|
|
824
|
+
- `AsyncProviderInjectionError` is removed; it can no longer happen.
|
|
825
|
+
- `Scope.Scoped` is removed. Only `Singleton` and `Transient` remain (`Transient`
|
|
826
|
+
keeps the value 2). Use `Transient`, or a child container
|
|
827
|
+
([`createChild`](#child-containers)) for per-request state. A scope value that
|
|
828
|
+
is not a `Scope` member (such as `1`, the old `Scoped`) throws
|
|
829
|
+
`InvalidScopeError` at bootstrap.
|
|
830
|
+
- `setParent()` and `getParent()` are removed. Replace
|
|
831
|
+
`NexusApplication.create(X).setParent(app).bootstrap()` by declaring `X` in
|
|
832
|
+
`children` of the module that owns it and calling `await app.createChild(X)`;
|
|
833
|
+
see [Child containers](#child-containers). A child now sees what its
|
|
834
|
+
declaring module sees, not the whole parent, and the parent closes its live
|
|
835
|
+
children.
|
|
836
|
+
- `get()` no longer returns a promise: drop the `await` for singletons and use
|
|
837
|
+
`await resolve()` for `Transient` and lazy-mode providers.
|
|
838
|
+
- `get()` now throws instead of returning `undefined` for unknown tokens. Pass
|
|
839
|
+
`{ optional: true }` to keep the old `undefined` result.
|
|
840
|
+
- `ModuleRef.get()` is synchronous too; the async variant is
|
|
841
|
+
`ModuleRef.resolve()`.
|
|
842
|
+
- `Test.compile()` now instantiates singletons and runs `onModuleInit`, so
|
|
843
|
+
errors from them surface from `compile()`.
|
|
844
|
+
- The module container returned by `compile()` has a synchronous `get()` and
|
|
845
|
+
a new async `resolve()`. Its `get()` throws `ProviderNotFoundError` for an
|
|
846
|
+
unknown token instead of resolving to `undefined` (pass
|
|
847
|
+
`{ optional: true }`), and throws `ProviderScopeError` for `Transient`
|
|
848
|
+
providers: use `await moduleRef.resolve(X)` for those.
|
|
849
|
+
- `get()` and `resolve()` called before `bootstrap()` throw
|
|
850
|
+
`ContainerNotCompiledError` instead of `ProviderNotFoundError`.
|
|
851
|
+
- `.lazy()` is deprecated: lazy modules (`lazy()` / `load()`) cover the
|
|
852
|
+
startup-time use case, and in `.lazy()` mode `get()` throws
|
|
853
|
+
`ProviderNotInstantiatedError`.
|
|
854
|
+
- The CLI service-spec template now generates `moduleRef.get<X>(X)` without
|
|
855
|
+
`await`.
|
|
856
|
+
|
|
127
857
|
## License
|
|
128
858
|
|
|
129
859
|
This project is licensed under the MIT License. See the LICENSE file for details.
|