@djodjonx/neo-syringe 1.1.5 → 1.2.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/.github/workflows/ci.yml +6 -5
- package/.github/workflows/docs.yml +59 -0
- package/CHANGELOG.md +27 -0
- package/README.md +74 -740
- package/dist/{GraphValidator-G0F4QiLk.cjs → GraphValidator-CV4VoJl0.cjs} +18 -10
- package/dist/{GraphValidator-C8ldJtNp.mjs → GraphValidator-DXqqkNdS.mjs} +18 -10
- package/dist/cli/index.cjs +16 -1
- package/dist/cli/index.mjs +16 -1
- package/dist/index.d.cts +31 -5
- package/dist/index.d.mts +31 -5
- package/dist/lsp/index.cjs +1 -1
- package/dist/lsp/index.mjs +1 -1
- package/dist/unplugin/index.cjs +33 -9
- package/dist/unplugin/index.d.cts +7 -5
- package/dist/unplugin/index.d.mts +7 -5
- package/dist/unplugin/index.mjs +33 -9
- package/docs/.vitepress/config.ts +109 -0
- package/docs/.vitepress/theme/custom.css +150 -0
- package/docs/.vitepress/theme/index.ts +17 -0
- package/docs/api/configuration.md +274 -0
- package/docs/api/functions.md +291 -0
- package/docs/api/types.md +158 -0
- package/docs/guide/basic-usage.md +267 -0
- package/docs/guide/cli.md +174 -0
- package/docs/guide/generated-code.md +284 -0
- package/docs/guide/getting-started.md +171 -0
- package/docs/guide/ide-plugin.md +203 -0
- package/docs/guide/injection-types.md +287 -0
- package/docs/guide/legacy-migration.md +333 -0
- package/docs/guide/lifecycle.md +223 -0
- package/docs/guide/parent-container.md +321 -0
- package/docs/guide/scoped-injections.md +271 -0
- package/docs/guide/what-is-neo-syringe.md +162 -0
- package/docs/guide/why-neo-syringe.md +219 -0
- package/docs/index.md +138 -0
- package/docs/public/logo.png +0 -0
- package/package.json +15 -12
- package/src/analyzer/Analyzer.ts +20 -10
- package/src/analyzer/types.ts +55 -49
- package/src/cli/index.ts +15 -0
- package/src/generator/Generator.ts +24 -2
- package/src/generator/GraphValidator.ts +6 -2
- package/src/types.ts +30 -4
- package/src/unplugin/index.ts +13 -41
- package/tests/analyzer/Analyzer.test.ts +4 -4
- package/tests/analyzer/AnalyzerDeclarative.test.ts +1 -1
- package/tests/analyzer/Factory.test.ts +2 -2
- package/tests/analyzer/Scoped.test.ts +434 -0
- package/tests/cli/cli.test.ts +91 -0
- package/tests/e2e/container-integration.test.ts +21 -21
- package/tests/e2e/generated-code.test.ts +7 -7
- package/tests/e2e/scoped.test.ts +370 -0
- package/tests/e2e/snapshots.test.ts +2 -2
- package/tests/e2e/standalone.test.ts +2 -2
- package/tests/generator/ExternalGenerator.test.ts +1 -1
- package/tests/generator/FactoryGenerator.test.ts +6 -6
- package/tests/generator/Generator.test.ts +2 -2
- package/tests/generator/GeneratorDeclarative.test.ts +1 -1
- package/tests/generator/GraphValidator.test.ts +1 -1
- package/tsconfig.json +2 -1
- package/typedoc.json +0 -5
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Legacy Migration
|
|
2
|
+
|
|
3
|
+
Bridge existing DI containers (tsyringe, InversifyJS) while migrating to Neo-Syringe.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
You don't have to migrate everything at once. Neo-Syringe can delegate resolution to any container that has a `resolve()` method.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Bridge your existing container
|
|
11
|
+
export const container = defineBuilderConfig({
|
|
12
|
+
useContainer: legacyContainer, // Delegate to legacy
|
|
13
|
+
injections: [
|
|
14
|
+
{ token: NewService } // New services in Neo-Syringe
|
|
15
|
+
]
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## With tsyringe
|
|
20
|
+
|
|
21
|
+
### Step 1: Keep Your Existing Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// legacy-container.ts (existing tsyringe code)
|
|
25
|
+
import 'reflect-metadata';
|
|
26
|
+
import { container, injectable } from 'tsyringe';
|
|
27
|
+
|
|
28
|
+
@injectable()
|
|
29
|
+
export class AuthService {
|
|
30
|
+
validateToken(token: string) { return true; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@injectable()
|
|
34
|
+
export class LegacyUserRepository {
|
|
35
|
+
findById(id: string) { return { id, name: 'John' }; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Register in tsyringe
|
|
39
|
+
container.registerSingleton(AuthService);
|
|
40
|
+
container.registerSingleton(LegacyUserRepository);
|
|
41
|
+
|
|
42
|
+
export { container as legacyContainer };
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Step 2: Declare Legacy Tokens
|
|
46
|
+
|
|
47
|
+
Use `declareContainerTokens` for type-safety:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// container.ts
|
|
51
|
+
import { defineBuilderConfig, declareContainerTokens, useInterface } from '@djodjonx/neo-syringe';
|
|
52
|
+
import { legacyContainer, AuthService, LegacyUserRepository } from './legacy-container';
|
|
53
|
+
|
|
54
|
+
// Declare what the legacy container provides
|
|
55
|
+
const legacy = declareContainerTokens<{
|
|
56
|
+
AuthService: AuthService;
|
|
57
|
+
LegacyUserRepository: LegacyUserRepository;
|
|
58
|
+
}>(legacyContainer);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Step 3: Bridge and Extend
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// New services using Neo-Syringe
|
|
65
|
+
interface ILogger {
|
|
66
|
+
log(msg: string): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ConsoleLogger implements ILogger {
|
|
70
|
+
log(msg: string) { console.log(msg); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class UserService {
|
|
74
|
+
constructor(
|
|
75
|
+
private auth: AuthService, // From legacy!
|
|
76
|
+
private repo: LegacyUserRepository, // From legacy!
|
|
77
|
+
private logger: ILogger // From neo-syringe
|
|
78
|
+
) {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const appContainer = defineBuilderConfig({
|
|
82
|
+
name: 'AppContainer',
|
|
83
|
+
useContainer: legacy, // 👈 Bridge to legacy
|
|
84
|
+
injections: [
|
|
85
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger },
|
|
86
|
+
{ token: UserService }
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step 4: Use It
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// main.ts
|
|
95
|
+
import { appContainer } from './container';
|
|
96
|
+
|
|
97
|
+
const userService = appContainer.resolve(UserService);
|
|
98
|
+
// ✅ AuthService and LegacyUserRepository come from tsyringe
|
|
99
|
+
// ✅ ILogger comes from neo-syringe
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## With InversifyJS
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// legacy-inversify.ts
|
|
106
|
+
import 'reflect-metadata';
|
|
107
|
+
import { Container, injectable } from 'inversify';
|
|
108
|
+
|
|
109
|
+
@injectable()
|
|
110
|
+
class DatabaseConnection {
|
|
111
|
+
query(sql: string) { return []; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const inversifyContainer = new Container();
|
|
115
|
+
inversifyContainer.bind(DatabaseConnection).toSelf().inSingletonScope();
|
|
116
|
+
|
|
117
|
+
export { inversifyContainer, DatabaseConnection };
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// container.ts
|
|
122
|
+
import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
|
|
123
|
+
import { inversifyContainer, DatabaseConnection } from './legacy-inversify';
|
|
124
|
+
|
|
125
|
+
const legacy = declareContainerTokens<{
|
|
126
|
+
DatabaseConnection: DatabaseConnection;
|
|
127
|
+
}>(inversifyContainer);
|
|
128
|
+
|
|
129
|
+
class ReportService {
|
|
130
|
+
constructor(private db: DatabaseConnection) {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const container = defineBuilderConfig({
|
|
134
|
+
useContainer: legacy,
|
|
135
|
+
injections: [
|
|
136
|
+
{ token: ReportService }
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## With Awilix
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// legacy-awilix.ts
|
|
145
|
+
import { createContainer, asClass } from 'awilix';
|
|
146
|
+
|
|
147
|
+
class EmailService {
|
|
148
|
+
send(to: string, subject: string) { /* ... */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const awilixContainer = createContainer();
|
|
152
|
+
awilixContainer.register({
|
|
153
|
+
emailService: asClass(EmailService).singleton()
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Awilix uses different API, create wrapper
|
|
157
|
+
export const legacyContainer = {
|
|
158
|
+
resolve(token: any) {
|
|
159
|
+
return awilixContainer.resolve(token.name ?? token);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// container.ts
|
|
166
|
+
import { defineBuilderConfig, declareContainerTokens } from '@djodjonx/neo-syringe';
|
|
167
|
+
import { legacyContainer, EmailService } from './legacy-awilix';
|
|
168
|
+
|
|
169
|
+
const legacy = declareContainerTokens<{
|
|
170
|
+
EmailService: EmailService;
|
|
171
|
+
}>(legacyContainer);
|
|
172
|
+
|
|
173
|
+
export const container = defineBuilderConfig({
|
|
174
|
+
useContainer: legacy,
|
|
175
|
+
injections: [
|
|
176
|
+
{ token: NotificationService } // Uses EmailService from Awilix
|
|
177
|
+
]
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## How It Works
|
|
182
|
+
|
|
183
|
+
### At Compile-Time
|
|
184
|
+
|
|
185
|
+
1. `declareContainerTokens<T>()` is analyzed
|
|
186
|
+
2. Type `T` properties are extracted (e.g., `{ AuthService, UserRepo }`)
|
|
187
|
+
3. These tokens are added to `parentProvidedTokens`
|
|
188
|
+
4. GraphValidator accepts them as valid dependencies
|
|
189
|
+
5. Generator outputs: `new NeoContainer(undefined, [legacyContainer])`
|
|
190
|
+
|
|
191
|
+
### At Runtime
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Generated code (simplified)
|
|
195
|
+
class NeoContainer {
|
|
196
|
+
constructor(
|
|
197
|
+
private parent?: any,
|
|
198
|
+
private legacy?: any[] // ← Your tsyringe/inversify container
|
|
199
|
+
) {}
|
|
200
|
+
|
|
201
|
+
resolve(token: any): any {
|
|
202
|
+
// 1. Try local resolution
|
|
203
|
+
const local = this.resolveLocal(token);
|
|
204
|
+
if (local !== undefined) return local;
|
|
205
|
+
|
|
206
|
+
// 2. Delegate to parent (Neo-Syringe container)
|
|
207
|
+
if (this.parent) {
|
|
208
|
+
try { return this.parent.resolve(token); }
|
|
209
|
+
catch { /* continue */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 3. Delegate to legacy containers
|
|
213
|
+
if (this.legacy) {
|
|
214
|
+
for (const container of this.legacy) {
|
|
215
|
+
try { return container.resolve(token); } // ← Calls tsyringe!
|
|
216
|
+
catch { /* try next */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new Error(`Service not found: ${token}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Validation
|
|
226
|
+
|
|
227
|
+
Neo-Syringe validates legacy bindings at compile-time:
|
|
228
|
+
|
|
229
|
+
| Check | Description |
|
|
230
|
+
|-------|-------------|
|
|
231
|
+
| ✅ Missing binding | Error if dependency not in local OR legacy container |
|
|
232
|
+
| ✅ Duplicate detection | Error if token already registered in legacy |
|
|
233
|
+
| ✅ Type safety | `declareContainerTokens<T>()` provides TypeScript types |
|
|
234
|
+
|
|
235
|
+
## Migration Strategy
|
|
236
|
+
|
|
237
|
+
### Phase 1: Bridge Everything
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const legacy = declareContainerTokens<{
|
|
241
|
+
ServiceA: ServiceA;
|
|
242
|
+
ServiceB: ServiceB;
|
|
243
|
+
ServiceC: ServiceC;
|
|
244
|
+
// ... all services
|
|
245
|
+
}>(tsyringeContainer);
|
|
246
|
+
|
|
247
|
+
export const container = defineBuilderConfig({
|
|
248
|
+
useContainer: legacy,
|
|
249
|
+
injections: [] // Nothing new yet
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Phase 2: New Services in Neo-Syringe
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
export const container = defineBuilderConfig({
|
|
257
|
+
useContainer: legacy,
|
|
258
|
+
injections: [
|
|
259
|
+
{ token: NewServiceD },
|
|
260
|
+
{ token: NewServiceE }
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Phase 3: Migrate One at a Time
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Remove ServiceA from legacy declaration
|
|
269
|
+
const legacy = declareContainerTokens<{
|
|
270
|
+
ServiceB: ServiceB;
|
|
271
|
+
ServiceC: ServiceC;
|
|
272
|
+
}>(tsyringeContainer);
|
|
273
|
+
|
|
274
|
+
// Add to Neo-Syringe
|
|
275
|
+
export const container = defineBuilderConfig({
|
|
276
|
+
useContainer: legacy,
|
|
277
|
+
injections: [
|
|
278
|
+
{ token: ServiceA }, // Migrated!
|
|
279
|
+
{ token: NewServiceD },
|
|
280
|
+
{ token: NewServiceE }
|
|
281
|
+
]
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Phase 4: Complete Migration
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// No more legacy!
|
|
289
|
+
export const container = defineBuilderConfig({
|
|
290
|
+
injections: [
|
|
291
|
+
{ token: ServiceA },
|
|
292
|
+
{ token: ServiceB },
|
|
293
|
+
{ token: ServiceC },
|
|
294
|
+
{ token: NewServiceD },
|
|
295
|
+
{ token: NewServiceE }
|
|
296
|
+
]
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Tips
|
|
301
|
+
|
|
302
|
+
### Keep Legacy Container Isolated
|
|
303
|
+
|
|
304
|
+
Put legacy code in a separate file that you can eventually delete:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
src/
|
|
308
|
+
├── legacy/
|
|
309
|
+
│ └── container.ts # Will be deleted later
|
|
310
|
+
├── container.ts # Neo-Syringe
|
|
311
|
+
└── services/
|
|
312
|
+
├── legacy/ # To be migrated
|
|
313
|
+
└── new/ # Pure TypeScript
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Test Both Paths
|
|
317
|
+
|
|
318
|
+
Ensure services work whether resolved from legacy or Neo-Syringe:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
describe('UserService', () => {
|
|
322
|
+
it('works from legacy container', () => {
|
|
323
|
+
const service = legacyContainer.resolve(UserService);
|
|
324
|
+
expect(service).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('works from neo-syringe container', () => {
|
|
328
|
+
const service = container.resolve(UserService);
|
|
329
|
+
expect(service).toBeDefined();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
Control how instances are created and managed.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Neo-Syringe supports two lifecycle modes:
|
|
8
|
+
|
|
9
|
+
| Lifecycle | Behavior | Default |
|
|
10
|
+
|-----------|----------|---------|
|
|
11
|
+
| `singleton` | One instance per container | ✅ Yes |
|
|
12
|
+
| `transient` | New instance every `resolve()` | No |
|
|
13
|
+
|
|
14
|
+
## Singleton (Default)
|
|
15
|
+
|
|
16
|
+
By default, all services are **singletons**. The container creates one instance and reuses it.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
export const container = defineBuilderConfig({
|
|
20
|
+
injections: [
|
|
21
|
+
{ token: UserService } // Singleton by default
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const a = container.resolve(UserService);
|
|
26
|
+
const b = container.resolve(UserService);
|
|
27
|
+
|
|
28
|
+
console.log(a === b); // true - same instance!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### When to Use Singleton
|
|
32
|
+
|
|
33
|
+
- ✅ Stateless services (most services)
|
|
34
|
+
- ✅ Database connections
|
|
35
|
+
- ✅ Configuration objects
|
|
36
|
+
- ✅ Loggers
|
|
37
|
+
- ✅ HTTP clients
|
|
38
|
+
|
|
39
|
+
## Transient
|
|
40
|
+
|
|
41
|
+
Use `lifecycle: 'transient'` for a new instance on every resolution.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export const container = defineBuilderConfig({
|
|
45
|
+
injections: [
|
|
46
|
+
{ token: RequestContext, lifecycle: 'transient' }
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const a = container.resolve(RequestContext);
|
|
51
|
+
const b = container.resolve(RequestContext);
|
|
52
|
+
|
|
53
|
+
console.log(a === b); // false - different instances!
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### When to Use Transient
|
|
57
|
+
|
|
58
|
+
- ✅ Request-scoped objects
|
|
59
|
+
- ✅ Stateful objects that should be isolated
|
|
60
|
+
- ✅ Objects with unique IDs
|
|
61
|
+
- ✅ Builder/Factory patterns
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
### Request Context
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
class RequestContext {
|
|
69
|
+
readonly id = crypto.randomUUID();
|
|
70
|
+
readonly timestamp = new Date();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const container = defineBuilderConfig({
|
|
74
|
+
injections: [
|
|
75
|
+
{ token: RequestContext, lifecycle: 'transient' }
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Each request gets its own context
|
|
80
|
+
app.use((req, res, next) => {
|
|
81
|
+
req.context = container.resolve(RequestContext);
|
|
82
|
+
next();
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Factory with Transient
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
{
|
|
90
|
+
token: useInterface<IRequest>(),
|
|
91
|
+
provider: () => ({
|
|
92
|
+
id: crypto.randomUUID(),
|
|
93
|
+
timestamp: Date.now()
|
|
94
|
+
}),
|
|
95
|
+
lifecycle: 'transient'
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mixed Lifecycles
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
class Logger {
|
|
103
|
+
// Singleton - shared across all services
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class UserService {
|
|
107
|
+
constructor(private logger: Logger) {}
|
|
108
|
+
// Singleton - one instance
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class RequestHandler {
|
|
112
|
+
constructor(private userService: UserService) {}
|
|
113
|
+
// Transient - new instance per request
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const container = defineBuilderConfig({
|
|
117
|
+
injections: [
|
|
118
|
+
{ token: Logger }, // singleton
|
|
119
|
+
{ token: UserService }, // singleton
|
|
120
|
+
{ token: RequestHandler, lifecycle: 'transient' } // transient
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// RequestHandler is new each time, but shares the same UserService
|
|
125
|
+
const handler1 = container.resolve(RequestHandler);
|
|
126
|
+
const handler2 = container.resolve(RequestHandler);
|
|
127
|
+
|
|
128
|
+
console.log(handler1 === handler2); // false
|
|
129
|
+
console.log(handler1.userService === handler2.userService); // true
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Generated Code
|
|
133
|
+
|
|
134
|
+
Understanding how lifecycle affects the generated code:
|
|
135
|
+
|
|
136
|
+
### Singleton
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Generated for singleton
|
|
140
|
+
private resolveLocal(token: any): any {
|
|
141
|
+
if (token === UserService) {
|
|
142
|
+
if (!this.instances.has(UserService)) {
|
|
143
|
+
this.instances.set(UserService, create_UserService(this));
|
|
144
|
+
}
|
|
145
|
+
return this.instances.get(UserService);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Transient
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Generated for transient
|
|
154
|
+
private resolveLocal(token: any): any {
|
|
155
|
+
if (token === RequestContext) {
|
|
156
|
+
return create_RequestContext(this); // No caching!
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Scoped Lifecycle with Parent Containers
|
|
162
|
+
|
|
163
|
+
When using `scoped: true`, you can define a **different lifecycle** than the parent:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Parent: ILogger is singleton
|
|
167
|
+
const parent = defineBuilderConfig({
|
|
168
|
+
injections: [
|
|
169
|
+
{ token: useInterface<ILogger>(), provider: ConsoleLogger, lifecycle: 'singleton' }
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Child: Override with transient lifecycle
|
|
174
|
+
const child = defineBuilderConfig({
|
|
175
|
+
useContainer: parent,
|
|
176
|
+
injections: [
|
|
177
|
+
{
|
|
178
|
+
token: useInterface<ILogger>(),
|
|
179
|
+
provider: FileLogger,
|
|
180
|
+
lifecycle: 'transient', // Different from parent!
|
|
181
|
+
scoped: true
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// In parent: same logger instance
|
|
187
|
+
const a = parent.resolve(useInterface<ILogger>());
|
|
188
|
+
const b = parent.resolve(useInterface<ILogger>());
|
|
189
|
+
console.log(a === b); // true
|
|
190
|
+
|
|
191
|
+
// In child: new instance each time
|
|
192
|
+
const c = child.resolve(useInterface<ILogger>());
|
|
193
|
+
const d = child.resolve(useInterface<ILogger>());
|
|
194
|
+
console.log(c === d); // false
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Best Practices
|
|
198
|
+
|
|
199
|
+
### 1. Default to Singleton
|
|
200
|
+
|
|
201
|
+
Most services should be singletons. Only use transient when you have a specific reason.
|
|
202
|
+
|
|
203
|
+
### 2. Transient for Stateful Objects
|
|
204
|
+
|
|
205
|
+
If an object holds request-specific state, make it transient:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
class ShoppingCart {
|
|
209
|
+
items: CartItem[] = [];
|
|
210
|
+
|
|
211
|
+
addItem(item: CartItem) {
|
|
212
|
+
this.items.push(item);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Each user gets their own cart
|
|
217
|
+
{ token: ShoppingCart, lifecycle: 'transient' }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 3. Consider Memory
|
|
221
|
+
|
|
222
|
+
Transient objects are not cached, which can increase memory churn. Profile your application if you're creating many transient instances.
|
|
223
|
+
|