@navios/di 0.5.0 → 0.6.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/CHANGELOG.md +146 -0
- package/README.md +196 -219
- package/docs/README.md +69 -11
- package/docs/api-reference.md +281 -117
- package/docs/container.md +220 -56
- package/docs/examples/request-scope-example.mts +2 -2
- package/docs/factory.md +3 -8
- package/docs/getting-started.md +37 -8
- package/docs/migration.md +318 -37
- package/docs/request-contexts.md +263 -175
- package/docs/scopes.md +79 -42
- package/lib/browser/index.d.mts +1577 -0
- package/lib/browser/index.d.mts.map +1 -0
- package/lib/browser/index.mjs +3012 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-S_qX2VLI.d.mts +1211 -0
- package/lib/index-S_qX2VLI.d.mts.map +1 -0
- package/lib/index-fKPuT65j.d.cts +1206 -0
- package/lib/index-fKPuT65j.d.cts.map +1 -0
- package/lib/index.cjs +389 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +376 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +371 -78
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +325 -63
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +9 -0
- package/lib/testing/index.d.cts +2 -0
- package/lib/testing/index.d.mts +2 -2
- package/lib/testing/index.mjs +2 -72
- package/lib/testing-BMGmmxH7.cjs +2895 -0
- package/lib/testing-BMGmmxH7.cjs.map +1 -0
- package/lib/testing-DCXz8AJD.mjs +2655 -0
- package/lib/testing-DCXz8AJD.mjs.map +1 -0
- package/package.json +26 -4
- package/project.json +2 -2
- package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
- package/src/__tests__/async-local-storage.spec.mts +333 -0
- package/src/__tests__/container.spec.mts +30 -25
- package/src/__tests__/e2e.browser.spec.mts +790 -0
- package/src/__tests__/e2e.spec.mts +1222 -0
- package/src/__tests__/errors.spec.mts +6 -6
- package/src/__tests__/factory.spec.mts +1 -1
- package/src/__tests__/get-injectors.spec.mts +1 -1
- package/src/__tests__/injectable.spec.mts +1 -1
- package/src/__tests__/injection-token.spec.mts +1 -1
- package/src/__tests__/library-findings.spec.mts +563 -0
- package/src/__tests__/registry.spec.mts +2 -2
- package/src/__tests__/request-scope.spec.mts +266 -274
- package/src/__tests__/service-instantiator.spec.mts +19 -17
- package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
- package/src/__tests__/service-locator-manager.spec.mts +15 -15
- package/src/__tests__/service-locator.spec.mts +167 -244
- package/src/__tests__/unified-api.spec.mts +27 -27
- package/src/__type-tests__/factory.spec-d.mts +2 -2
- package/src/__type-tests__/inject.spec-d.mts +2 -2
- package/src/__type-tests__/injectable.spec-d.mts +1 -1
- package/src/browser.mts +16 -0
- package/src/container/container.mts +319 -0
- package/src/container/index.mts +2 -0
- package/src/container/scoped-container.mts +350 -0
- package/src/decorators/factory.decorator.mts +4 -4
- package/src/decorators/injectable.decorator.mts +5 -5
- package/src/errors/di-error.mts +13 -7
- package/src/errors/index.mts +0 -8
- package/src/index.mts +156 -15
- package/src/interfaces/container.interface.mts +82 -0
- package/src/interfaces/factory.interface.mts +2 -2
- package/src/interfaces/index.mts +1 -0
- package/src/internal/context/async-local-storage.mts +120 -0
- package/src/internal/context/factory-context.mts +18 -0
- package/src/internal/context/index.mts +3 -0
- package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
- package/src/internal/context/resolution-context.mts +63 -0
- package/src/internal/context/sync-local-storage.mts +51 -0
- package/src/internal/core/index.mts +5 -0
- package/src/internal/core/instance-resolver.mts +641 -0
- package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
- package/src/internal/core/invalidator.mts +437 -0
- package/src/internal/core/service-locator.mts +202 -0
- package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
- package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
- package/src/internal/holder/holder-manager.mts +85 -0
- package/src/internal/holder/holder-storage.interface.mts +116 -0
- package/src/internal/holder/index.mts +6 -0
- package/src/internal/holder/instance-holder.mts +109 -0
- package/src/internal/holder/request-storage.mts +134 -0
- package/src/internal/holder/singleton-storage.mts +105 -0
- package/src/internal/index.mts +4 -0
- package/src/internal/lifecycle/circular-detector.mts +77 -0
- package/src/internal/lifecycle/index.mts +2 -0
- package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +12 -5
- package/src/testing/__tests__/test-container.spec.mts +2 -2
- package/src/testing/test-container.mts +4 -4
- package/src/token/index.mts +2 -0
- package/src/{injection-token.mts → token/injection-token.mts} +1 -1
- package/src/{registry.mts → token/registry.mts} +1 -1
- package/src/utils/get-injectable-token.mts +1 -1
- package/src/utils/get-injectors.mts +32 -15
- package/src/utils/types.mts +1 -1
- package/tsdown.config.mts +67 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1283
- package/lib/_tsup-dts-rollup.d.ts +0 -1283
- package/lib/chunk-44F3LXW5.mjs +0 -2043
- package/lib/chunk-44F3LXW5.mjs.map +0 -1
- package/lib/index.d.ts +0 -78
- package/lib/index.js +0 -2127
- package/lib/index.js.map +0 -1
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/index.js +0 -2060
- package/lib/testing/index.js.map +0 -1
- package/lib/testing/index.mjs.map +0 -1
- package/src/container.mts +0 -227
- package/src/factory-context.mts +0 -8
- package/src/instance-resolver.mts +0 -559
- package/src/request-context-manager.mts +0 -149
- package/src/service-invalidator.mts +0 -429
- package/src/service-locator-instance-holder.mts +0 -70
- package/src/service-locator-manager.mts +0 -85
- package/src/service-locator.mts +0 -246
- package/tsup.config.mts +0 -12
- /package/src/{injector.mts → injectors.mts} +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for @navios/di running with SyncLocalStorage (browser mode).
|
|
3
|
+
*
|
|
4
|
+
* This file runs all the same E2E tests but with the sync polyfill forced,
|
|
5
|
+
* simulating browser environment behavior.
|
|
6
|
+
*
|
|
7
|
+
* The key difference: In browser mode, AsyncLocalStorage context does NOT
|
|
8
|
+
* propagate across async boundaries. This affects circular dependency detection
|
|
9
|
+
* for async operations, but synchronous DI operations should work identically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
+
|
|
14
|
+
import { Container } from '../container/container.mjs'
|
|
15
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
16
|
+
import { InjectableScope } from '../enums/index.mjs'
|
|
17
|
+
import type { OnServiceDestroy } from '../interfaces/on-service-destroy.interface.mjs'
|
|
18
|
+
import type { OnServiceInit } from '../interfaces/on-service-init.interface.mjs'
|
|
19
|
+
import { Registry } from '../token/registry.mjs'
|
|
20
|
+
import { getInjectors } from '../utils/get-injectors.mjs'
|
|
21
|
+
import { __testing__, isUsingNativeAsyncLocalStorage } from '../internal/context/async-local-storage.mjs'
|
|
22
|
+
|
|
23
|
+
// Force sync mode for all tests in this file
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
__testing__.forceSyncMode()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
__testing__.reset()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// TEST UTILITIES
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
function delay(ms: number): Promise<void> {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createTestSetup() {
|
|
41
|
+
const registry = new Registry()
|
|
42
|
+
const injectors = getInjectors()
|
|
43
|
+
const container = new Container(registry, null, injectors)
|
|
44
|
+
|
|
45
|
+
return { registry, injectors, container }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// VERIFY BROWSER MODE
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
describe('Browser Mode Verification', () => {
|
|
53
|
+
it('should be using sync polyfill', () => {
|
|
54
|
+
expect(isUsingNativeAsyncLocalStorage()).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// SECTION 1: BASIC SETUP TESTS (Browser Mode)
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
describe('E2E Browser: Basic Setup', () => {
|
|
63
|
+
let registry: Registry
|
|
64
|
+
let container: Container
|
|
65
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
const setup = createTestSetup()
|
|
69
|
+
registry = setup.registry
|
|
70
|
+
container = setup.container
|
|
71
|
+
injectors = setup.injectors
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterEach(async () => {
|
|
75
|
+
await container.dispose()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('Simple service registration and resolution', () => {
|
|
79
|
+
it('should register and resolve a simple singleton service', async () => {
|
|
80
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
81
|
+
class SimpleService {
|
|
82
|
+
getValue() {
|
|
83
|
+
return 'hello'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const instance = await container.get(SimpleService)
|
|
88
|
+
expect(instance).toBeInstanceOf(SimpleService)
|
|
89
|
+
expect(instance.getValue()).toBe('hello')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should return the same instance for singleton services', async () => {
|
|
93
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
94
|
+
class SingletonService {
|
|
95
|
+
id = Math.random()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const instance1 = await container.get(SingletonService)
|
|
99
|
+
const instance2 = await container.get(SingletonService)
|
|
100
|
+
|
|
101
|
+
expect(instance1).toBe(instance2)
|
|
102
|
+
expect(instance1.id).toBe(instance2.id)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should return different instances for transient services', async () => {
|
|
106
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
107
|
+
class TransientService {
|
|
108
|
+
id = Math.random()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const instance1 = await container.get(TransientService)
|
|
112
|
+
const instance2 = await container.get(TransientService)
|
|
113
|
+
|
|
114
|
+
expect(instance1).not.toBe(instance2)
|
|
115
|
+
expect(instance1.id).not.toBe(instance2.id)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('Service with dependencies', () => {
|
|
120
|
+
it('should resolve a service that depends on another singleton', async () => {
|
|
121
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
122
|
+
class DatabaseService {
|
|
123
|
+
connect() {
|
|
124
|
+
return 'connected'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
129
|
+
class UserRepository {
|
|
130
|
+
private db = injectors.inject(DatabaseService)
|
|
131
|
+
|
|
132
|
+
async getDatabase() {
|
|
133
|
+
return this.db
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const repo = await container.get(UserRepository)
|
|
138
|
+
const db = await repo.getDatabase()
|
|
139
|
+
expect(db).toBeInstanceOf(DatabaseService)
|
|
140
|
+
expect(db.connect()).toBe('connected')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should resolve deep dependency chains', async () => {
|
|
144
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
145
|
+
class ServiceLevel1 {
|
|
146
|
+
name = 'Level1'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
150
|
+
class ServiceLevel2 {
|
|
151
|
+
private level1 = injectors.inject(ServiceLevel1)
|
|
152
|
+
name = 'Level2'
|
|
153
|
+
|
|
154
|
+
async getLevel1() {
|
|
155
|
+
return this.level1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
160
|
+
class ServiceLevel3 {
|
|
161
|
+
private level2 = injectors.inject(ServiceLevel2)
|
|
162
|
+
name = 'Level3'
|
|
163
|
+
|
|
164
|
+
async getLevel2() {
|
|
165
|
+
return this.level2
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const level3 = await container.get(ServiceLevel3)
|
|
170
|
+
expect(level3.name).toBe('Level3')
|
|
171
|
+
|
|
172
|
+
const level2 = await level3.getLevel2()
|
|
173
|
+
expect(level2.name).toBe('Level2')
|
|
174
|
+
|
|
175
|
+
const level1 = await level2.getLevel1()
|
|
176
|
+
expect(level1.name).toBe('Level1')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// SECTION 2: MIXED SCOPES TESTS (Browser Mode)
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
describe('E2E Browser: Mixed Scopes', () => {
|
|
186
|
+
let registry: Registry
|
|
187
|
+
let container: Container
|
|
188
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
const setup = createTestSetup()
|
|
192
|
+
registry = setup.registry
|
|
193
|
+
container = setup.container
|
|
194
|
+
injectors = setup.injectors
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
afterEach(async () => {
|
|
198
|
+
await container.dispose()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('Singleton with Request-scoped dependencies', () => {
|
|
202
|
+
it('should create different request-scoped instances for different requests', async () => {
|
|
203
|
+
let instanceCount = 0
|
|
204
|
+
|
|
205
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
206
|
+
class RequestService {
|
|
207
|
+
id = ++instanceCount
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// First request
|
|
211
|
+
const scoped1 = container.beginRequest('request-1')
|
|
212
|
+
const service1 = await scoped1.get(RequestService)
|
|
213
|
+
|
|
214
|
+
// Second request
|
|
215
|
+
const scoped2 = container.beginRequest('request-2')
|
|
216
|
+
const service2 = await scoped2.get(RequestService)
|
|
217
|
+
|
|
218
|
+
expect(service1.id).toBe(1)
|
|
219
|
+
expect(service2.id).toBe(2)
|
|
220
|
+
expect(service1).not.toBe(service2)
|
|
221
|
+
|
|
222
|
+
await scoped1.endRequest()
|
|
223
|
+
await scoped2.endRequest()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('Transient within different scopes', () => {
|
|
228
|
+
it('should create new transient instance every time regardless of scope', async () => {
|
|
229
|
+
let instanceCount = 0
|
|
230
|
+
|
|
231
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
232
|
+
class TransientService {
|
|
233
|
+
id = ++instanceCount
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// From main container
|
|
237
|
+
const t1 = await container.get(TransientService)
|
|
238
|
+
const t2 = await container.get(TransientService)
|
|
239
|
+
|
|
240
|
+
// From scoped container
|
|
241
|
+
const scoped = container.beginRequest('request-1')
|
|
242
|
+
const t3 = await scoped.get(TransientService)
|
|
243
|
+
const t4 = await scoped.get(TransientService)
|
|
244
|
+
|
|
245
|
+
expect(t1.id).toBe(1)
|
|
246
|
+
expect(t2.id).toBe(2)
|
|
247
|
+
expect(t3.id).toBe(3)
|
|
248
|
+
expect(t4.id).toBe(4)
|
|
249
|
+
|
|
250
|
+
await scoped.endRequest()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('Complex dependency graphs', () => {
|
|
255
|
+
it('should handle complex dependency graph with all scope types', async () => {
|
|
256
|
+
const creationOrder: string[] = []
|
|
257
|
+
|
|
258
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
259
|
+
class DatabaseConnection {
|
|
260
|
+
constructor() {
|
|
261
|
+
creationOrder.push('DatabaseConnection')
|
|
262
|
+
}
|
|
263
|
+
query() {
|
|
264
|
+
return 'query result'
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
269
|
+
class RequestLogger {
|
|
270
|
+
private db = injectors.inject(DatabaseConnection)
|
|
271
|
+
|
|
272
|
+
constructor() {
|
|
273
|
+
creationOrder.push('RequestLogger')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async log(msg: string) {
|
|
277
|
+
const db = await this.db
|
|
278
|
+
return `${msg} - ${db.query()}`
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@Injectable({ scope: InjectableScope.Transient, registry })
|
|
283
|
+
class ActionHandler {
|
|
284
|
+
private logger = injectors.inject(RequestLogger)
|
|
285
|
+
|
|
286
|
+
constructor() {
|
|
287
|
+
creationOrder.push('ActionHandler')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async handle() {
|
|
291
|
+
const log = await this.logger
|
|
292
|
+
return log.log('action')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
297
|
+
class RequestProcessor {
|
|
298
|
+
private action = injectors.inject(ActionHandler)
|
|
299
|
+
|
|
300
|
+
constructor() {
|
|
301
|
+
creationOrder.push('RequestProcessor')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async process() {
|
|
305
|
+
const handler = await this.action
|
|
306
|
+
return handler.handle()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const scoped = container.beginRequest('request-1')
|
|
311
|
+
|
|
312
|
+
const processor = await scoped.get(RequestProcessor)
|
|
313
|
+
const result = await processor.process()
|
|
314
|
+
|
|
315
|
+
expect(result).toBe('action - query result')
|
|
316
|
+
expect(creationOrder).toContain('DatabaseConnection')
|
|
317
|
+
expect(creationOrder).toContain('RequestLogger')
|
|
318
|
+
expect(creationOrder).toContain('ActionHandler')
|
|
319
|
+
expect(creationOrder).toContain('RequestProcessor')
|
|
320
|
+
|
|
321
|
+
await scoped.endRequest()
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// SECTION 3: CONCURRENT REQUEST TESTS (Browser Mode)
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
describe('E2E Browser: Concurrent Requests', () => {
|
|
331
|
+
let registry: Registry
|
|
332
|
+
let container: Container
|
|
333
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
334
|
+
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
const setup = createTestSetup()
|
|
337
|
+
registry = setup.registry
|
|
338
|
+
container = setup.container
|
|
339
|
+
injectors = setup.injectors
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
afterEach(async () => {
|
|
343
|
+
await container.dispose()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('Parallel request processing', () => {
|
|
347
|
+
it('should share singleton across all concurrent requests', async () => {
|
|
348
|
+
let singletonCreationCount = 0
|
|
349
|
+
|
|
350
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
351
|
+
class SharedCache {
|
|
352
|
+
id = ++singletonCreationCount
|
|
353
|
+
private data = new Map<string, string>()
|
|
354
|
+
|
|
355
|
+
set(key: string, value: string) {
|
|
356
|
+
this.data.set(key, value)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
get(key: string) {
|
|
360
|
+
return this.data.get(key)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
365
|
+
class RequestService {
|
|
366
|
+
private cache = injectors.inject(SharedCache)
|
|
367
|
+
|
|
368
|
+
async setInCache(key: string, value: string) {
|
|
369
|
+
const c = await this.cache
|
|
370
|
+
c.set(key, value)
|
|
371
|
+
return c.id
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async getFromCache(key: string) {
|
|
375
|
+
const c = await this.cache
|
|
376
|
+
return c.get(key)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Start multiple requests that all use the cache
|
|
381
|
+
const requests = Array.from({ length: 10 }, async (_, i) => {
|
|
382
|
+
const scoped = container.beginRequest(`request-${i}`)
|
|
383
|
+
const service = await scoped.get(RequestService)
|
|
384
|
+
const cacheId = await service.setInCache(`key-${i}`, `value-${i}`)
|
|
385
|
+
await scoped.endRequest()
|
|
386
|
+
return cacheId
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const cacheIds = await Promise.all(requests)
|
|
390
|
+
|
|
391
|
+
// All requests should have used the same singleton
|
|
392
|
+
expect(singletonCreationCount).toBe(1)
|
|
393
|
+
expect(new Set(cacheIds).size).toBe(1)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('should handle race conditions when creating the same singleton', async () => {
|
|
397
|
+
let creationCount = 0
|
|
398
|
+
let constructorCallCount = 0
|
|
399
|
+
|
|
400
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
401
|
+
class SlowSingleton {
|
|
402
|
+
id: number
|
|
403
|
+
|
|
404
|
+
constructor() {
|
|
405
|
+
constructorCallCount++
|
|
406
|
+
this.id = ++creationCount
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Try to get the same singleton from many places concurrently
|
|
411
|
+
const results = await Promise.all([
|
|
412
|
+
container.get(SlowSingleton),
|
|
413
|
+
container.get(SlowSingleton),
|
|
414
|
+
container.get(SlowSingleton),
|
|
415
|
+
container.get(SlowSingleton),
|
|
416
|
+
container.get(SlowSingleton),
|
|
417
|
+
])
|
|
418
|
+
|
|
419
|
+
// All should be the same instance
|
|
420
|
+
const uniqueInstances = new Set(results)
|
|
421
|
+
expect(uniqueInstances.size).toBe(1)
|
|
422
|
+
expect(creationCount).toBe(1)
|
|
423
|
+
expect(constructorCallCount).toBe(1)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// ============================================================================
|
|
429
|
+
// SECTION 4: SERVICE LIFECYCLE TESTS (Browser Mode)
|
|
430
|
+
// ============================================================================
|
|
431
|
+
|
|
432
|
+
describe('E2E Browser: Service Lifecycle', () => {
|
|
433
|
+
let registry: Registry
|
|
434
|
+
let container: Container
|
|
435
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
436
|
+
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
const setup = createTestSetup()
|
|
439
|
+
registry = setup.registry
|
|
440
|
+
container = setup.container
|
|
441
|
+
injectors = setup.injectors
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
afterEach(async () => {
|
|
445
|
+
await container.dispose()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe('OnServiceInit lifecycle', () => {
|
|
449
|
+
it('should call onServiceInit when service is created', async () => {
|
|
450
|
+
const initSpy = vi.fn()
|
|
451
|
+
|
|
452
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
453
|
+
class ServiceWithInit implements OnServiceInit {
|
|
454
|
+
initialized = false
|
|
455
|
+
|
|
456
|
+
async onServiceInit() {
|
|
457
|
+
initSpy()
|
|
458
|
+
this.initialized = true
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const service = await container.get(ServiceWithInit)
|
|
463
|
+
|
|
464
|
+
expect(initSpy).toHaveBeenCalledTimes(1)
|
|
465
|
+
expect(service.initialized).toBe(true)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should call onServiceInit for request-scoped services in each request', async () => {
|
|
469
|
+
let initCount = 0
|
|
470
|
+
|
|
471
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
472
|
+
class RequestServiceWithInit implements OnServiceInit {
|
|
473
|
+
initOrder = 0
|
|
474
|
+
|
|
475
|
+
onServiceInit() {
|
|
476
|
+
this.initOrder = ++initCount
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const scoped1 = container.beginRequest('request-1')
|
|
481
|
+
const service1 = await scoped1.get(RequestServiceWithInit)
|
|
482
|
+
|
|
483
|
+
const scoped2 = container.beginRequest('request-2')
|
|
484
|
+
const service2 = await scoped2.get(RequestServiceWithInit)
|
|
485
|
+
|
|
486
|
+
expect(service1.initOrder).toBe(1)
|
|
487
|
+
expect(service2.initOrder).toBe(2)
|
|
488
|
+
expect(initCount).toBe(2)
|
|
489
|
+
|
|
490
|
+
await scoped1.endRequest()
|
|
491
|
+
await scoped2.endRequest()
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
describe('OnServiceDestroy lifecycle', () => {
|
|
496
|
+
it('should call onServiceDestroy when container is disposed', async () => {
|
|
497
|
+
const destroySpy = vi.fn()
|
|
498
|
+
|
|
499
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
500
|
+
class ServiceWithDestroy implements OnServiceDestroy {
|
|
501
|
+
onServiceDestroy() {
|
|
502
|
+
destroySpy()
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await container.get(ServiceWithDestroy)
|
|
507
|
+
expect(destroySpy).not.toHaveBeenCalled()
|
|
508
|
+
|
|
509
|
+
await container.dispose()
|
|
510
|
+
expect(destroySpy).toHaveBeenCalledTimes(1)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('should call onServiceDestroy for request-scoped services when request ends', async () => {
|
|
514
|
+
const destroySpy = vi.fn()
|
|
515
|
+
|
|
516
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
517
|
+
class RequestServiceWithDestroy implements OnServiceDestroy {
|
|
518
|
+
onServiceDestroy() {
|
|
519
|
+
destroySpy()
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const scoped = container.beginRequest('request-1')
|
|
524
|
+
await scoped.get(RequestServiceWithDestroy)
|
|
525
|
+
|
|
526
|
+
expect(destroySpy).not.toHaveBeenCalled()
|
|
527
|
+
|
|
528
|
+
await scoped.endRequest()
|
|
529
|
+
expect(destroySpy).toHaveBeenCalledTimes(1)
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('should handle async onServiceDestroy', async () => {
|
|
533
|
+
const events: string[] = []
|
|
534
|
+
|
|
535
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
536
|
+
class SlowDestroyService implements OnServiceDestroy {
|
|
537
|
+
async onServiceDestroy() {
|
|
538
|
+
events.push('destroy-start')
|
|
539
|
+
await delay(10)
|
|
540
|
+
events.push('destroy-end')
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
await container.get(SlowDestroyService)
|
|
545
|
+
await container.dispose()
|
|
546
|
+
|
|
547
|
+
expect(events).toEqual(['destroy-start', 'destroy-end'])
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
describe('Combined lifecycle methods', () => {
|
|
552
|
+
it('should call lifecycle methods in correct order', async () => {
|
|
553
|
+
const events: string[] = []
|
|
554
|
+
|
|
555
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
556
|
+
class FullLifecycleService implements OnServiceInit, OnServiceDestroy {
|
|
557
|
+
onServiceInit() {
|
|
558
|
+
events.push('init')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
onServiceDestroy() {
|
|
562
|
+
events.push('destroy')
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const scoped = container.beginRequest('request-1')
|
|
567
|
+
await scoped.get(FullLifecycleService)
|
|
568
|
+
events.push('service-used')
|
|
569
|
+
await scoped.endRequest()
|
|
570
|
+
|
|
571
|
+
expect(events).toEqual(['init', 'service-used', 'destroy'])
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// SECTION 5: INVALIDATION TESTS (Browser Mode)
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
describe('E2E Browser: Service Invalidation', () => {
|
|
581
|
+
let registry: Registry
|
|
582
|
+
let container: Container
|
|
583
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
584
|
+
|
|
585
|
+
beforeEach(() => {
|
|
586
|
+
const setup = createTestSetup()
|
|
587
|
+
registry = setup.registry
|
|
588
|
+
container = setup.container
|
|
589
|
+
injectors = setup.injectors
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
afterEach(async () => {
|
|
593
|
+
await container.dispose()
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
describe('Basic invalidation', () => {
|
|
597
|
+
it('should destroy service when invalidated', async () => {
|
|
598
|
+
const destroySpy = vi.fn()
|
|
599
|
+
let instanceCount = 0
|
|
600
|
+
|
|
601
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
602
|
+
class CachingService implements OnServiceDestroy {
|
|
603
|
+
id = ++instanceCount
|
|
604
|
+
|
|
605
|
+
onServiceDestroy() {
|
|
606
|
+
destroySpy(this.id)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const service1 = await container.get(CachingService)
|
|
611
|
+
expect(service1.id).toBe(1)
|
|
612
|
+
|
|
613
|
+
await container.invalidate(service1)
|
|
614
|
+
expect(destroySpy).toHaveBeenCalledWith(1)
|
|
615
|
+
|
|
616
|
+
// Getting service again should create new instance
|
|
617
|
+
const service2 = await container.get(CachingService)
|
|
618
|
+
expect(service2.id).toBe(2)
|
|
619
|
+
expect(service2).not.toBe(service1)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('should cascade invalidation to dependents', async () => {
|
|
623
|
+
const destroyOrder: string[] = []
|
|
624
|
+
|
|
625
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
626
|
+
class BaseService implements OnServiceDestroy {
|
|
627
|
+
onServiceDestroy() {
|
|
628
|
+
destroyOrder.push('BaseService')
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
633
|
+
class DependentService implements OnServiceDestroy {
|
|
634
|
+
private base = injectors.inject(BaseService)
|
|
635
|
+
|
|
636
|
+
async getBase() {
|
|
637
|
+
return this.base
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
onServiceDestroy() {
|
|
641
|
+
destroyOrder.push('DependentService')
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const dependent = await container.get(DependentService)
|
|
646
|
+
await dependent.getBase() // ensure base is created
|
|
647
|
+
|
|
648
|
+
const base = await container.get(BaseService)
|
|
649
|
+
await container.invalidate(base)
|
|
650
|
+
|
|
651
|
+
// Both should be destroyed
|
|
652
|
+
expect(destroyOrder).toContain('BaseService')
|
|
653
|
+
expect(destroyOrder).toContain('DependentService')
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
// ============================================================================
|
|
659
|
+
// SECTION 6: ERROR HANDLING TESTS (Browser Mode)
|
|
660
|
+
// ============================================================================
|
|
661
|
+
|
|
662
|
+
describe('E2E Browser: Error Handling', () => {
|
|
663
|
+
let registry: Registry
|
|
664
|
+
let container: Container
|
|
665
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
666
|
+
|
|
667
|
+
beforeEach(() => {
|
|
668
|
+
const setup = createTestSetup()
|
|
669
|
+
registry = setup.registry
|
|
670
|
+
container = setup.container
|
|
671
|
+
injectors = setup.injectors
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
afterEach(async () => {
|
|
675
|
+
try {
|
|
676
|
+
await container.dispose()
|
|
677
|
+
} catch {
|
|
678
|
+
// Ignore dispose errors in error handling tests
|
|
679
|
+
}
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
describe('Resolution errors', () => {
|
|
683
|
+
it('should throw when resolving request-scoped service outside request context', async () => {
|
|
684
|
+
@Injectable({ scope: InjectableScope.Request, registry })
|
|
685
|
+
class RequestOnlyService {}
|
|
686
|
+
|
|
687
|
+
await expect(container.get(RequestOnlyService)).rejects.toThrow()
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it('should throw when using duplicate request IDs', async () => {
|
|
691
|
+
const scoped1 = container.beginRequest('duplicate-id')
|
|
692
|
+
|
|
693
|
+
expect(() => container.beginRequest('duplicate-id')).toThrow()
|
|
694
|
+
|
|
695
|
+
await scoped1.endRequest()
|
|
696
|
+
})
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
describe('Initialization errors', () => {
|
|
700
|
+
it('should propagate errors from onServiceInit', async () => {
|
|
701
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
702
|
+
class FailingInitService implements OnServiceInit {
|
|
703
|
+
onServiceInit() {
|
|
704
|
+
throw new Error('Init failed!')
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
await expect(container.get(FailingInitService)).rejects.toThrow(
|
|
709
|
+
'Init failed!',
|
|
710
|
+
)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should propagate constructor errors', async () => {
|
|
714
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
715
|
+
class FailingConstructorService {
|
|
716
|
+
constructor() {
|
|
717
|
+
throw new Error('Constructor failed!')
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
await expect(container.get(FailingConstructorService)).rejects.toThrow(
|
|
722
|
+
'Constructor failed!',
|
|
723
|
+
)
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// ============================================================================
|
|
729
|
+
// SECTION 7: ADVANCED SCENARIOS (Browser Mode)
|
|
730
|
+
// ============================================================================
|
|
731
|
+
|
|
732
|
+
describe('E2E Browser: Advanced Scenarios', () => {
|
|
733
|
+
let registry: Registry
|
|
734
|
+
let container: Container
|
|
735
|
+
let injectors: ReturnType<typeof getInjectors>
|
|
736
|
+
|
|
737
|
+
beforeEach(() => {
|
|
738
|
+
const setup = createTestSetup()
|
|
739
|
+
registry = setup.registry
|
|
740
|
+
container = setup.container
|
|
741
|
+
injectors = setup.injectors
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
afterEach(async () => {
|
|
745
|
+
await container.dispose()
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
describe('tryGetSync functionality', () => {
|
|
749
|
+
it('should return null when service does not exist', () => {
|
|
750
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
751
|
+
class LazyService {}
|
|
752
|
+
|
|
753
|
+
const result = container.tryGetSync<LazyService>(LazyService)
|
|
754
|
+
expect(result).toBeNull()
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('should return instance when service exists', async () => {
|
|
758
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
759
|
+
class EagerService {
|
|
760
|
+
value = 42
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// First create the service
|
|
764
|
+
await container.get(EagerService)
|
|
765
|
+
|
|
766
|
+
// Now sync get should work
|
|
767
|
+
const result = container.tryGetSync<EagerService>(EagerService)
|
|
768
|
+
expect(result).not.toBeNull()
|
|
769
|
+
expect(result!.value).toBe(42)
|
|
770
|
+
})
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
describe('Container self-registration', () => {
|
|
774
|
+
it('should allow injecting the Container itself', async () => {
|
|
775
|
+
@Injectable({ scope: InjectableScope.Singleton, registry })
|
|
776
|
+
class ContainerAwareService {
|
|
777
|
+
private container = injectors.inject(Container)
|
|
778
|
+
|
|
779
|
+
async getContainer() {
|
|
780
|
+
return this.container
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const service = await container.get(ContainerAwareService)
|
|
785
|
+
const injectedContainer = await service.getContainer()
|
|
786
|
+
|
|
787
|
+
expect(injectedContainer).toBe(container)
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
})
|