sandly 0.0.2 → 0.1.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/dist/index.d.ts +817 -403
- package/dist/index.js +426 -328
- package/package.json +75 -76
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
|
|
3
3
|
//#region src/tag.ts
|
|
4
|
+
Symbol("InjectSource");
|
|
4
5
|
/**
|
|
5
6
|
* Internal symbol used to identify tagged types within the dependency injection system.
|
|
6
7
|
* This symbol is used as a property key to attach metadata to both value tags and class tags.
|
|
@@ -31,8 +32,6 @@ const Tag = {
|
|
|
31
32
|
class Tagged {
|
|
32
33
|
static [TagId] = id;
|
|
33
34
|
[TagId] = id;
|
|
34
|
-
/** @internal */
|
|
35
|
-
__type;
|
|
36
35
|
}
|
|
37
36
|
return Tagged;
|
|
38
37
|
},
|
|
@@ -89,14 +88,30 @@ var BaseError = class BaseError extends Error {
|
|
|
89
88
|
* try {
|
|
90
89
|
* await container.get(SomeService);
|
|
91
90
|
* } catch (error) {
|
|
92
|
-
* if (error instanceof
|
|
91
|
+
* if (error instanceof ContainerError) {
|
|
93
92
|
* console.error('DI Error:', error.message);
|
|
94
93
|
* console.error('Details:', error.detail);
|
|
95
94
|
* }
|
|
96
95
|
* }
|
|
97
96
|
* ```
|
|
98
97
|
*/
|
|
99
|
-
var
|
|
98
|
+
var ContainerError = class extends BaseError {};
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
101
|
+
*
|
|
102
|
+
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
103
|
+
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
104
|
+
* by existing dependencies.
|
|
105
|
+
*/
|
|
106
|
+
var DependencyAlreadyInstantiatedError = class extends ContainerError {};
|
|
107
|
+
/**
|
|
108
|
+
* Error thrown when attempting to use a container that has been destroyed.
|
|
109
|
+
*
|
|
110
|
+
* This error occurs when calling `container.get()`, `container.register()`, or `container.destroy()`
|
|
111
|
+
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
112
|
+
* is being used after it has been destroyed.
|
|
113
|
+
*/
|
|
114
|
+
var ContainerDestroyedError = class extends ContainerError {};
|
|
100
115
|
/**
|
|
101
116
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
102
117
|
*
|
|
@@ -117,7 +132,7 @@ var DependencyContainerError = class extends BaseError {};
|
|
|
117
132
|
* }
|
|
118
133
|
* ```
|
|
119
134
|
*/
|
|
120
|
-
var UnknownDependencyError = class extends
|
|
135
|
+
var UnknownDependencyError = class extends ContainerError {
|
|
121
136
|
/**
|
|
122
137
|
* @internal
|
|
123
138
|
* Creates an UnknownDependencyError for the given tag.
|
|
@@ -141,11 +156,11 @@ var UnknownDependencyError = class extends DependencyContainerError {
|
|
|
141
156
|
* class ServiceB extends Tag.Class('ServiceB') {}
|
|
142
157
|
*
|
|
143
158
|
* const c = container()
|
|
144
|
-
* .register(ServiceA, async (
|
|
145
|
-
* new ServiceA(await
|
|
159
|
+
* .register(ServiceA, async (ctx) =>
|
|
160
|
+
* new ServiceA(await ctx.get(ServiceB)) // Depends on B
|
|
146
161
|
* )
|
|
147
|
-
* .register(ServiceB, async (
|
|
148
|
-
* new ServiceB(await
|
|
162
|
+
* .register(ServiceB, async (ctx) =>
|
|
163
|
+
* new ServiceB(await ctx.get(ServiceA)) // Depends on A - CIRCULAR!
|
|
149
164
|
* );
|
|
150
165
|
*
|
|
151
166
|
* try {
|
|
@@ -158,7 +173,7 @@ var UnknownDependencyError = class extends DependencyContainerError {
|
|
|
158
173
|
* }
|
|
159
174
|
* ```
|
|
160
175
|
*/
|
|
161
|
-
var CircularDependencyError = class extends
|
|
176
|
+
var CircularDependencyError = class extends ContainerError {
|
|
162
177
|
/**
|
|
163
178
|
* @internal
|
|
164
179
|
* Creates a CircularDependencyError with the dependency chain information.
|
|
@@ -198,7 +213,7 @@ var CircularDependencyError = class extends DependencyContainerError {
|
|
|
198
213
|
* }
|
|
199
214
|
* ```
|
|
200
215
|
*/
|
|
201
|
-
var DependencyCreationError = class extends
|
|
216
|
+
var DependencyCreationError = class extends ContainerError {
|
|
202
217
|
/**
|
|
203
218
|
* @internal
|
|
204
219
|
* Creates a DependencyCreationError wrapping the original factory error.
|
|
@@ -207,7 +222,7 @@ var DependencyCreationError = class extends DependencyContainerError {
|
|
|
207
222
|
* @param error - The original error thrown by the factory function
|
|
208
223
|
*/
|
|
209
224
|
constructor(tag, error) {
|
|
210
|
-
super(`Error creating instance of ${Tag.id(tag)}
|
|
225
|
+
super(`Error creating instance of ${Tag.id(tag)}`, {
|
|
211
226
|
cause: error,
|
|
212
227
|
detail: { tag: Tag.id(tag) }
|
|
213
228
|
});
|
|
@@ -225,17 +240,17 @@ var DependencyCreationError = class extends DependencyContainerError {
|
|
|
225
240
|
* try {
|
|
226
241
|
* await container.destroy();
|
|
227
242
|
* } catch (error) {
|
|
228
|
-
* if (error instanceof
|
|
243
|
+
* if (error instanceof DependencyFinalizationError) {
|
|
229
244
|
* console.error('Some finalizers failed');
|
|
230
245
|
* console.error('Error details:', error.detail.errors);
|
|
231
246
|
* }
|
|
232
247
|
* }
|
|
233
248
|
* ```
|
|
234
249
|
*/
|
|
235
|
-
var
|
|
250
|
+
var DependencyFinalizationError = class extends ContainerError {
|
|
236
251
|
/**
|
|
237
252
|
* @internal
|
|
238
|
-
* Creates a
|
|
253
|
+
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
239
254
|
*
|
|
240
255
|
* @param errors - Array of errors thrown by individual finalizers
|
|
241
256
|
*/
|
|
@@ -257,46 +272,6 @@ var DependencyContainerFinalizationError = class extends DependencyContainerErro
|
|
|
257
272
|
*/
|
|
258
273
|
const resolutionChain = new AsyncLocalStorage();
|
|
259
274
|
/**
|
|
260
|
-
* Shared logic for dependency resolution that handles caching, circular dependency detection,
|
|
261
|
-
* and error handling. Used by both BasicDependencyContainer and ScopedDependencyContainer.
|
|
262
|
-
* @internal
|
|
263
|
-
*/
|
|
264
|
-
async function resolveDependency(tag, cache, factories, container$1) {
|
|
265
|
-
const cached = cache.get(tag);
|
|
266
|
-
if (cached !== void 0) return cached;
|
|
267
|
-
const currentChain = resolutionChain.getStore() ?? [];
|
|
268
|
-
if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
|
|
269
|
-
const factory = factories.get(tag);
|
|
270
|
-
if (factory === void 0) return Promise.reject(new UnknownDependencyError(tag));
|
|
271
|
-
const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
|
|
272
|
-
try {
|
|
273
|
-
const instance = await factory(container$1);
|
|
274
|
-
return instance;
|
|
275
|
-
} catch (error) {
|
|
276
|
-
if (error instanceof CircularDependencyError) throw error;
|
|
277
|
-
throw new DependencyCreationError(tag, error);
|
|
278
|
-
}
|
|
279
|
-
}).catch((error) => {
|
|
280
|
-
cache.delete(tag);
|
|
281
|
-
throw error;
|
|
282
|
-
});
|
|
283
|
-
cache.set(tag, instancePromise);
|
|
284
|
-
return instancePromise;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Shared logic for running finalizers and handling cleanup errors.
|
|
288
|
-
* @internal
|
|
289
|
-
*/
|
|
290
|
-
async function runFinalizers(finalizers, cache) {
|
|
291
|
-
const promises = Array.from(finalizers.entries()).filter(([tag]) => cache.has(tag)).map(async ([tag, finalizer]) => {
|
|
292
|
-
const dep = await cache.get(tag);
|
|
293
|
-
return finalizer(dep);
|
|
294
|
-
});
|
|
295
|
-
const results = await Promise.allSettled(promises);
|
|
296
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
297
|
-
if (failures.length > 0) throw new DependencyContainerFinalizationError(failures.map((result) => result.reason));
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
275
|
* A type-safe dependency injection container that manages service instantiation,
|
|
301
276
|
* caching, and lifecycle management with support for async dependencies and
|
|
302
277
|
* circular dependency detection.
|
|
@@ -309,7 +284,7 @@ async function runFinalizers(finalizers, cache) {
|
|
|
309
284
|
*
|
|
310
285
|
* @example Basic usage with class tags
|
|
311
286
|
* ```typescript
|
|
312
|
-
* import { container, Tag } from '
|
|
287
|
+
* import { container, Tag } from 'sandly';
|
|
313
288
|
*
|
|
314
289
|
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
315
290
|
* query() { return 'data'; }
|
|
@@ -322,8 +297,8 @@ async function runFinalizers(finalizers, cache) {
|
|
|
322
297
|
*
|
|
323
298
|
* const c = container()
|
|
324
299
|
* .register(DatabaseService, () => new DatabaseService())
|
|
325
|
-
* .register(UserService, async (
|
|
326
|
-
* new UserService(await
|
|
300
|
+
* .register(UserService, async (ctx) =>
|
|
301
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
327
302
|
* );
|
|
328
303
|
*
|
|
329
304
|
* const userService = await c.get(UserService);
|
|
@@ -363,7 +338,7 @@ async function runFinalizers(finalizers, cache) {
|
|
|
363
338
|
* await c.destroy(); // Calls all finalizers
|
|
364
339
|
* ```
|
|
365
340
|
*/
|
|
366
|
-
var Container = class {
|
|
341
|
+
var Container = class Container {
|
|
367
342
|
/**
|
|
368
343
|
* Cache of instantiated dependencies as promises.
|
|
369
344
|
* Ensures singleton behavior and supports concurrent access.
|
|
@@ -381,18 +356,27 @@ var Container = class {
|
|
|
381
356
|
*/
|
|
382
357
|
finalizers = /* @__PURE__ */ new Map();
|
|
383
358
|
/**
|
|
359
|
+
* Flag indicating whether this container has been destroyed.
|
|
360
|
+
* @internal
|
|
361
|
+
*/
|
|
362
|
+
isDestroyed = false;
|
|
363
|
+
/**
|
|
384
364
|
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
385
365
|
*
|
|
386
366
|
* The factory function receives the current container instance and must return the
|
|
387
367
|
* service instance (or a Promise of it). The container tracks the registration at
|
|
388
368
|
* the type level, ensuring type safety for subsequent `.get()` calls.
|
|
389
369
|
*
|
|
370
|
+
* If a dependency is already registered, this method will override it unless the
|
|
371
|
+
* dependency has already been instantiated, in which case it will throw an error.
|
|
372
|
+
*
|
|
390
373
|
* @template T - The dependency tag being registered
|
|
391
374
|
* @param tag - The dependency tag (class or value tag)
|
|
392
375
|
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
393
376
|
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
394
377
|
* @returns A new container instance with the dependency registered
|
|
395
|
-
* @throws {
|
|
378
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
379
|
+
* @throws {Error} If the dependency has already been instantiated
|
|
396
380
|
*
|
|
397
381
|
* @example Registering a simple service
|
|
398
382
|
* ```typescript
|
|
@@ -415,14 +399,21 @@ var Container = class {
|
|
|
415
399
|
* const c = container()
|
|
416
400
|
* .register(DatabaseService, () => new DatabaseService())
|
|
417
401
|
* .register(LoggerService, () => new LoggerService())
|
|
418
|
-
* .register(UserService, async (
|
|
402
|
+
* .register(UserService, async (ctx) =>
|
|
419
403
|
* new UserService(
|
|
420
|
-
* await
|
|
421
|
-
* await
|
|
404
|
+
* await ctx.get(DatabaseService),
|
|
405
|
+
* await ctx.get(LoggerService)
|
|
422
406
|
* )
|
|
423
407
|
* );
|
|
424
408
|
* ```
|
|
425
409
|
*
|
|
410
|
+
* @example Overriding a dependency
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const c = container()
|
|
413
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
414
|
+
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
426
417
|
* @example Using value tags
|
|
427
418
|
* ```typescript
|
|
428
419
|
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
@@ -451,35 +442,43 @@ var Container = class {
|
|
|
451
442
|
* );
|
|
452
443
|
* ```
|
|
453
444
|
*/
|
|
454
|
-
register(tag,
|
|
455
|
-
if (this.
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
this.factories.set(tag,
|
|
459
|
-
this.finalizers.
|
|
445
|
+
register(tag, spec) {
|
|
446
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot register dependencies on a destroyed container");
|
|
447
|
+
if (this.has(tag) && this.exists(tag)) throw new DependencyAlreadyInstantiatedError(`Cannot register dependency ${Tag.id(tag)} - it has already been instantiated. Registration must happen before any instantiation occurs, as cached instances would still be used by existing dependencies.`);
|
|
448
|
+
if (typeof spec === "function") {
|
|
449
|
+
this.factories.set(tag, spec);
|
|
450
|
+
this.finalizers.delete(tag);
|
|
451
|
+
} else {
|
|
452
|
+
this.factories.set(tag, spec.factory);
|
|
453
|
+
this.finalizers.set(tag, spec.finalizer);
|
|
460
454
|
}
|
|
461
455
|
return this;
|
|
462
456
|
}
|
|
463
457
|
/**
|
|
464
|
-
* Checks if a dependency has been
|
|
458
|
+
* Checks if a dependency has been registered in the container.
|
|
465
459
|
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
460
|
+
* This returns `true` if the dependency has been registered via `.register()`,
|
|
461
|
+
* regardless of whether it has been instantiated yet.
|
|
468
462
|
*
|
|
469
463
|
* @param tag - The dependency tag to check
|
|
470
|
-
* @returns `true` if the dependency has been
|
|
464
|
+
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
471
465
|
*
|
|
472
466
|
* @example
|
|
473
467
|
* ```typescript
|
|
474
468
|
* const c = container().register(DatabaseService, () => new DatabaseService());
|
|
475
|
-
*
|
|
476
|
-
* console.log(c.has(DatabaseService)); // false - not instantiated yet
|
|
477
|
-
*
|
|
478
|
-
* await c.get(DatabaseService);
|
|
479
|
-
* console.log(c.has(DatabaseService)); // true - now instantiated and cached
|
|
469
|
+
* console.log(c.has(DatabaseService)); // true
|
|
480
470
|
* ```
|
|
481
471
|
*/
|
|
482
472
|
has(tag) {
|
|
473
|
+
return this.factories.has(tag);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Checks if a dependency has been instantiated (cached) in the container.
|
|
477
|
+
*
|
|
478
|
+
* @param tag - The dependency tag to check
|
|
479
|
+
* @returns true if the dependency has been instantiated, false otherwise
|
|
480
|
+
*/
|
|
481
|
+
exists(tag) {
|
|
483
482
|
return this.cache.has(tag);
|
|
484
483
|
}
|
|
485
484
|
/**
|
|
@@ -524,8 +523,8 @@ var Container = class {
|
|
|
524
523
|
* ```typescript
|
|
525
524
|
* const c = container()
|
|
526
525
|
* .register(DatabaseService, () => new DatabaseService())
|
|
527
|
-
* .register(UserService, async (
|
|
528
|
-
* const db = await
|
|
526
|
+
* .register(UserService, async (ctx) => {
|
|
527
|
+
* const db = await ctx.get(DatabaseService);
|
|
529
528
|
* return new UserService(db);
|
|
530
529
|
* });
|
|
531
530
|
*
|
|
@@ -533,14 +532,84 @@ var Container = class {
|
|
|
533
532
|
* ```
|
|
534
533
|
*/
|
|
535
534
|
async get(tag) {
|
|
536
|
-
|
|
535
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
|
|
536
|
+
const cached = this.cache.get(tag);
|
|
537
|
+
if (cached !== void 0) return cached;
|
|
538
|
+
const currentChain = resolutionChain.getStore() ?? [];
|
|
539
|
+
if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
|
|
540
|
+
const factory = this.factories.get(tag);
|
|
541
|
+
if (factory === void 0) throw new UnknownDependencyError(tag);
|
|
542
|
+
const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
|
|
543
|
+
try {
|
|
544
|
+
const instance = await factory(this);
|
|
545
|
+
return instance;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
throw new DependencyCreationError(tag, error);
|
|
548
|
+
}
|
|
549
|
+
}).catch((error) => {
|
|
550
|
+
this.cache.delete(tag);
|
|
551
|
+
throw error;
|
|
552
|
+
});
|
|
553
|
+
this.cache.set(tag, instancePromise);
|
|
554
|
+
return instancePromise;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Copies all registrations from this container to a target container.
|
|
558
|
+
*
|
|
559
|
+
* @internal
|
|
560
|
+
* @param target - The container to copy registrations to
|
|
561
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
562
|
+
*/
|
|
563
|
+
copyTo(target) {
|
|
564
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot copy registrations from a destroyed container");
|
|
565
|
+
for (const [tag, factory] of this.factories) {
|
|
566
|
+
const finalizer = this.finalizers.get(tag);
|
|
567
|
+
if (finalizer) target.register(tag, {
|
|
568
|
+
factory,
|
|
569
|
+
finalizer
|
|
570
|
+
});
|
|
571
|
+
else target.register(tag, factory);
|
|
572
|
+
}
|
|
537
573
|
}
|
|
538
574
|
/**
|
|
539
|
-
*
|
|
575
|
+
* Creates a new container by merging this container's registrations with another container.
|
|
540
576
|
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
577
|
+
* This method creates a new container that contains all registrations from both containers.
|
|
578
|
+
* If there are conflicts (same dependency registered in both containers), this
|
|
579
|
+
* container's registration will take precedence.
|
|
580
|
+
*
|
|
581
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
582
|
+
* The new container starts with an empty instance cache.
|
|
583
|
+
*
|
|
584
|
+
* @param other - The container to merge with
|
|
585
|
+
* @returns A new container with combined registrations
|
|
586
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
587
|
+
*
|
|
588
|
+
* @example Merging containers
|
|
589
|
+
* ```typescript
|
|
590
|
+
* const container1 = container()
|
|
591
|
+
* .register(DatabaseService, () => new DatabaseService());
|
|
592
|
+
*
|
|
593
|
+
* const container2 = container()
|
|
594
|
+
* .register(UserService, () => new UserService());
|
|
595
|
+
*
|
|
596
|
+
* const merged = container1.merge(container2);
|
|
597
|
+
* // merged has both DatabaseService and UserService
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
merge(other) {
|
|
601
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
|
|
602
|
+
const merged = new Container();
|
|
603
|
+
other.copyTo(merged);
|
|
604
|
+
this.copyTo(merged);
|
|
605
|
+
return merged;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
|
|
609
|
+
*
|
|
610
|
+
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
611
|
+
* Any subsequent calls to register(), get(), or destroy() will throw a ContainerError.
|
|
612
|
+
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
544
613
|
*
|
|
545
614
|
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
546
615
|
* for maximum cleanup performance.
|
|
@@ -552,9 +621,9 @@ var Container = class {
|
|
|
552
621
|
* dependencies are cleaned up.
|
|
553
622
|
*
|
|
554
623
|
* @returns Promise that resolves when all cleanup is complete
|
|
555
|
-
* @throws {
|
|
624
|
+
* @throws {DependencyFinalizationError} If any finalizers fail during cleanup
|
|
556
625
|
*
|
|
557
|
-
* @example Basic cleanup
|
|
626
|
+
* @example Basic cleanup
|
|
558
627
|
* ```typescript
|
|
559
628
|
* const c = container()
|
|
560
629
|
* .register(DatabaseConnection,
|
|
@@ -566,24 +635,32 @@ var Container = class {
|
|
|
566
635
|
* (conn) => conn.disconnect() // Finalizer
|
|
567
636
|
* );
|
|
568
637
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
* await c.destroy(); // Calls conn.disconnect(), clears cache
|
|
638
|
+
* const db = await c.get(DatabaseConnection);
|
|
639
|
+
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
572
640
|
*
|
|
573
|
-
* //
|
|
574
|
-
*
|
|
575
|
-
*
|
|
641
|
+
* // This will throw an error
|
|
642
|
+
* try {
|
|
643
|
+
* await c.get(DatabaseConnection);
|
|
644
|
+
* } catch (error) {
|
|
645
|
+
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
646
|
+
* }
|
|
576
647
|
* ```
|
|
577
648
|
*
|
|
578
|
-
* @example
|
|
649
|
+
* @example Application shutdown
|
|
579
650
|
* ```typescript
|
|
580
|
-
* const
|
|
651
|
+
* const appContainer = container()
|
|
652
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
653
|
+
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.get(DatabaseService)));
|
|
581
654
|
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
655
|
+
* // During application shutdown
|
|
656
|
+
* process.on('SIGTERM', async () => {
|
|
657
|
+
* try {
|
|
658
|
+
* await appContainer.destroy(); // Clean shutdown of all services
|
|
659
|
+
* } catch (error) {
|
|
660
|
+
* console.error('Error during shutdown:', error);
|
|
661
|
+
* }
|
|
662
|
+
* process.exit(0);
|
|
663
|
+
* });
|
|
587
664
|
* ```
|
|
588
665
|
*
|
|
589
666
|
* @example Handling cleanup errors
|
|
@@ -595,143 +672,25 @@ var Container = class {
|
|
|
595
672
|
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
596
673
|
* }
|
|
597
674
|
* }
|
|
598
|
-
* // Container is
|
|
675
|
+
* // Container is destroyed regardless of finalizer errors
|
|
599
676
|
* ```
|
|
600
677
|
*/
|
|
601
678
|
async destroy() {
|
|
679
|
+
if (this.isDestroyed) return;
|
|
602
680
|
try {
|
|
603
|
-
|
|
681
|
+
const promises = Array.from(this.finalizers.entries()).filter(([tag]) => this.cache.has(tag)).map(async ([tag, finalizer]) => {
|
|
682
|
+
const dep = await this.cache.get(tag);
|
|
683
|
+
return finalizer(dep);
|
|
684
|
+
});
|
|
685
|
+
const results = await Promise.allSettled(promises);
|
|
686
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
687
|
+
if (failures.length > 0) throw new DependencyFinalizationError(failures.map((result) => result.reason));
|
|
604
688
|
} finally {
|
|
689
|
+
this.isDestroyed = true;
|
|
605
690
|
this.cache.clear();
|
|
606
691
|
}
|
|
607
692
|
}
|
|
608
693
|
};
|
|
609
|
-
var ScopedContainer = class ScopedContainer {
|
|
610
|
-
scope;
|
|
611
|
-
parent;
|
|
612
|
-
children = [];
|
|
613
|
-
/**
|
|
614
|
-
* Cache of instantiated dependencies as promises for this scope.
|
|
615
|
-
* @internal
|
|
616
|
-
*/
|
|
617
|
-
cache = /* @__PURE__ */ new Map();
|
|
618
|
-
/**
|
|
619
|
-
* Factory functions for creating dependency instances in this scope.
|
|
620
|
-
* @internal
|
|
621
|
-
*/
|
|
622
|
-
factories = /* @__PURE__ */ new Map();
|
|
623
|
-
/**
|
|
624
|
-
* Finalizer functions for cleaning up dependencies when this scope is destroyed.
|
|
625
|
-
* @internal
|
|
626
|
-
*/
|
|
627
|
-
finalizers = /* @__PURE__ */ new Map();
|
|
628
|
-
constructor(parent, scope) {
|
|
629
|
-
this.parent = parent;
|
|
630
|
-
this.scope = scope;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Registers a dependency in the specified scope within this container's scope chain.
|
|
634
|
-
*
|
|
635
|
-
* If no scope is specified, registers in the current (leaf) scope. If a scope is specified,
|
|
636
|
-
* delegates to the parent container if the target scope doesn't match the current scope.
|
|
637
|
-
*
|
|
638
|
-
* This allows registering dependencies at different scope levels from any container
|
|
639
|
-
* in the scope chain, providing flexibility for dependency organization.
|
|
640
|
-
*
|
|
641
|
-
* @param tag - The dependency tag to register
|
|
642
|
-
* @param factory - Factory function to create the dependency
|
|
643
|
-
* @param finalizer - Optional cleanup function
|
|
644
|
-
* @param scope - Target scope for registration (defaults to current scope)
|
|
645
|
-
* @returns This container with updated type information
|
|
646
|
-
*
|
|
647
|
-
* @example Registering in different scopes
|
|
648
|
-
* ```typescript
|
|
649
|
-
* const runtime = scopedContainer('runtime');
|
|
650
|
-
* const request = runtime.child('request');
|
|
651
|
-
*
|
|
652
|
-
* // Register in current (request) scope
|
|
653
|
-
* request.register(RequestService, () => new RequestService());
|
|
654
|
-
*
|
|
655
|
-
* // Register in runtime scope from request container - delegates to parent
|
|
656
|
-
* request.register(DatabaseService, () => new DatabaseService(), undefined, 'runtime');
|
|
657
|
-
* ```
|
|
658
|
-
*/
|
|
659
|
-
register(tag, factoryOrLifecycle, scope) {
|
|
660
|
-
if (scope === void 0 || scope === this.scope) {
|
|
661
|
-
if (this.factories.has(tag)) throw new DependencyContainerError(`Dependency ${Tag.id(tag)} already registered in scope '${String(this.scope)}'`);
|
|
662
|
-
if (typeof factoryOrLifecycle === "function") this.factories.set(tag, factoryOrLifecycle);
|
|
663
|
-
else {
|
|
664
|
-
this.factories.set(tag, factoryOrLifecycle.factory);
|
|
665
|
-
this.finalizers.set(tag, factoryOrLifecycle.finalizer);
|
|
666
|
-
}
|
|
667
|
-
return this;
|
|
668
|
-
}
|
|
669
|
-
if (this.parent === null) throw new DependencyContainerError(`Scope '${String(scope)}' not found in container chain`);
|
|
670
|
-
this.parent.register(tag, factoryOrLifecycle, scope);
|
|
671
|
-
return this;
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
675
|
-
*
|
|
676
|
-
* This method checks the current scope first, then walks up the parent chain.
|
|
677
|
-
* Returns true only if the dependency has been created and cached somewhere in the scope hierarchy.
|
|
678
|
-
*/
|
|
679
|
-
has(tag) {
|
|
680
|
-
if (this.cache.has(tag)) return true;
|
|
681
|
-
return this.parent?.has(tag) ?? false;
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
685
|
-
*
|
|
686
|
-
* Resolution strategy:
|
|
687
|
-
* 1. Check cache in current scope
|
|
688
|
-
* 2. Check if factory exists in current scope - if so, create instance here
|
|
689
|
-
* 3. Otherwise, delegate to parent scope
|
|
690
|
-
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
691
|
-
*/
|
|
692
|
-
async get(tag) {
|
|
693
|
-
if (this.factories.has(tag)) return resolveDependency(tag, this.cache, this.factories, this);
|
|
694
|
-
if (this.parent !== null) return this.parent.get(tag);
|
|
695
|
-
throw new UnknownDependencyError(tag);
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
699
|
-
*
|
|
700
|
-
* This method ensures proper cleanup order while maintaining reusability:
|
|
701
|
-
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
702
|
-
* 2. Then calls finalizers for dependencies created in this scope
|
|
703
|
-
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
704
|
-
*
|
|
705
|
-
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
706
|
-
* before their dependents.
|
|
707
|
-
*/
|
|
708
|
-
async destroy() {
|
|
709
|
-
const allFailures = [];
|
|
710
|
-
try {
|
|
711
|
-
const childDestroyPromises = this.children.map((child) => child.destroy());
|
|
712
|
-
const childResults = await Promise.allSettled(childDestroyPromises);
|
|
713
|
-
const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
714
|
-
allFailures.push(...childFailures);
|
|
715
|
-
await runFinalizers(this.finalizers, this.cache);
|
|
716
|
-
} catch (error) {
|
|
717
|
-
allFailures.push(error);
|
|
718
|
-
} finally {
|
|
719
|
-
this.cache.clear();
|
|
720
|
-
}
|
|
721
|
-
if (allFailures.length > 0) throw new DependencyContainerFinalizationError(allFailures);
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Creates a child scoped container.
|
|
725
|
-
*
|
|
726
|
-
* Child containers inherit access to parent dependencies but maintain
|
|
727
|
-
* their own scope for new registrations and instance caching.
|
|
728
|
-
*/
|
|
729
|
-
child(scope) {
|
|
730
|
-
const child = new ScopedContainer(this, scope);
|
|
731
|
-
this.children.push(child);
|
|
732
|
-
return child;
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
694
|
/**
|
|
736
695
|
* Creates a new empty dependency injection container.
|
|
737
696
|
*
|
|
@@ -743,15 +702,15 @@ var ScopedContainer = class ScopedContainer {
|
|
|
743
702
|
*
|
|
744
703
|
* @example
|
|
745
704
|
* ```typescript
|
|
746
|
-
* import { container, Tag } from '
|
|
705
|
+
* import { container, Tag } from 'sandly';
|
|
747
706
|
*
|
|
748
707
|
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
749
708
|
* class UserService extends Tag.Class('UserService') {}
|
|
750
709
|
*
|
|
751
710
|
* const c = container()
|
|
752
711
|
* .register(DatabaseService, () => new DatabaseService())
|
|
753
|
-
* .register(UserService, async (
|
|
754
|
-
* new UserService(await
|
|
712
|
+
* .register(UserService, async (ctx) =>
|
|
713
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
755
714
|
* );
|
|
756
715
|
*
|
|
757
716
|
* const userService = await c.get(UserService);
|
|
@@ -760,9 +719,6 @@ var ScopedContainer = class ScopedContainer {
|
|
|
760
719
|
function container() {
|
|
761
720
|
return new Container();
|
|
762
721
|
}
|
|
763
|
-
function scopedContainer(scope) {
|
|
764
|
-
return new ScopedContainer(null, scope);
|
|
765
|
-
}
|
|
766
722
|
|
|
767
723
|
//#endregion
|
|
768
724
|
//#region src/layer.ts
|
|
@@ -772,14 +728,13 @@ function scopedContainer(scope) {
|
|
|
772
728
|
*
|
|
773
729
|
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
774
730
|
* @template TProvides - The union of dependency tags this layer registers/provides
|
|
775
|
-
* @template TParams - Optional parameters that can be passed to configure the layer
|
|
776
731
|
*
|
|
777
|
-
* @param register - Function that performs the dependency registrations. Receives a container
|
|
778
|
-
* @returns
|
|
732
|
+
* @param register - Function that performs the dependency registrations. Receives a container.
|
|
733
|
+
* @returns The layer instance.
|
|
779
734
|
*
|
|
780
|
-
* @example Simple layer
|
|
735
|
+
* @example Simple layer
|
|
781
736
|
* ```typescript
|
|
782
|
-
* import { layer, Tag } from '
|
|
737
|
+
* import { layer, Tag } from 'sandly';
|
|
783
738
|
*
|
|
784
739
|
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
785
740
|
* constructor(private url: string = 'sqlite://memory') {}
|
|
@@ -792,37 +747,7 @@ function scopedContainer(scope) {
|
|
|
792
747
|
* );
|
|
793
748
|
*
|
|
794
749
|
* // Usage
|
|
795
|
-
* const dbLayerInstance = databaseLayer
|
|
796
|
-
* ```
|
|
797
|
-
*
|
|
798
|
-
* @example Layer with dependencies
|
|
799
|
-
* ```typescript
|
|
800
|
-
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
801
|
-
*
|
|
802
|
-
* // Layer that requires ConfigTag and provides DatabaseService
|
|
803
|
-
* const databaseLayer = layer<typeof ConfigTag, typeof DatabaseService>((container) =>
|
|
804
|
-
* container.register(DatabaseService, async (c) => {
|
|
805
|
-
* const config = await c.get(ConfigTag);
|
|
806
|
-
* return new DatabaseService(config.dbUrl);
|
|
807
|
-
* })
|
|
808
|
-
* );
|
|
809
|
-
* ```
|
|
810
|
-
*
|
|
811
|
-
* @example Parameterized layer
|
|
812
|
-
* ```typescript
|
|
813
|
-
* interface DatabaseConfig {
|
|
814
|
-
* host: string;
|
|
815
|
-
* port: number;
|
|
816
|
-
* }
|
|
817
|
-
*
|
|
818
|
-
* // Layer that takes configuration parameters
|
|
819
|
-
* const databaseLayer = layer<never, typeof DatabaseService, DatabaseConfig>(
|
|
820
|
-
* (container, config) =>
|
|
821
|
-
* container.register(DatabaseService, () => new DatabaseService(config))
|
|
822
|
-
* );
|
|
823
|
-
*
|
|
824
|
-
* // Usage with parameters
|
|
825
|
-
* const dbLayerInstance = databaseLayer({ host: 'localhost', port: 5432 });
|
|
750
|
+
* const dbLayerInstance = databaseLayer;
|
|
826
751
|
* ```
|
|
827
752
|
*
|
|
828
753
|
* @example Complex application layer structure
|
|
@@ -836,73 +761,247 @@ function scopedContainer(scope) {
|
|
|
836
761
|
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
837
762
|
* (container) =>
|
|
838
763
|
* container
|
|
839
|
-
* .register(DatabaseService, async (
|
|
840
|
-
* .register(CacheService, async (
|
|
764
|
+
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.get(ConfigTag)))
|
|
765
|
+
* .register(CacheService, async (ctx) => new CacheService(await ctx.get(ConfigTag)))
|
|
841
766
|
* );
|
|
842
767
|
*
|
|
843
768
|
* // Service layer (requires infrastructure)
|
|
844
769
|
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
845
770
|
* (container) =>
|
|
846
|
-
* container.register(UserService, async (
|
|
847
|
-
* new UserService(await
|
|
771
|
+
* container.register(UserService, async (ctx) =>
|
|
772
|
+
* new UserService(await ctx.get(DatabaseService), await ctx.get(CacheService))
|
|
848
773
|
* )
|
|
849
774
|
* );
|
|
850
775
|
*
|
|
851
776
|
* // Compose the complete application
|
|
852
|
-
* const appLayer =
|
|
777
|
+
* const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
|
|
853
778
|
* ```
|
|
854
779
|
*/
|
|
855
780
|
function layer(register) {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
781
|
+
const layerImpl = {
|
|
782
|
+
register: (container$1) => register(container$1),
|
|
783
|
+
provide(dependency) {
|
|
784
|
+
return createProvidedLayer(dependency, layerImpl);
|
|
785
|
+
},
|
|
786
|
+
provideMerge(dependency) {
|
|
787
|
+
return createComposedLayer(dependency, layerImpl);
|
|
788
|
+
},
|
|
789
|
+
merge(other) {
|
|
790
|
+
return createMergedLayer(layerImpl, other);
|
|
791
|
+
}
|
|
867
792
|
};
|
|
868
|
-
return
|
|
793
|
+
return layerImpl;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Internal function to create a provided layer from two layers.
|
|
797
|
+
* This implements the `.provide()` method logic - only exposes target layer's provisions.
|
|
798
|
+
*
|
|
799
|
+
* @internal
|
|
800
|
+
*/
|
|
801
|
+
function createProvidedLayer(dependency, target) {
|
|
802
|
+
return createComposedLayer(dependency, target);
|
|
869
803
|
}
|
|
870
804
|
/**
|
|
871
805
|
* Internal function to create a composed layer from two layers.
|
|
872
|
-
* This implements the `.
|
|
806
|
+
* This implements the `.provideMerge()` method logic - exposes both layers' provisions.
|
|
873
807
|
*
|
|
874
808
|
* @internal
|
|
875
809
|
*/
|
|
876
|
-
function createComposedLayer(
|
|
810
|
+
function createComposedLayer(dependency, target) {
|
|
877
811
|
return layer((container$1) => {
|
|
878
|
-
const
|
|
879
|
-
return target.register(
|
|
880
|
-
})
|
|
812
|
+
const containerWithDependency = dependency.register(container$1);
|
|
813
|
+
return target.register(containerWithDependency);
|
|
814
|
+
});
|
|
881
815
|
}
|
|
882
816
|
/**
|
|
883
817
|
* Internal function to create a merged layer from two layers.
|
|
884
|
-
* This implements the `.
|
|
818
|
+
* This implements the `.merge()` method logic.
|
|
885
819
|
*
|
|
886
820
|
* @internal
|
|
887
821
|
*/
|
|
888
822
|
function createMergedLayer(layer1, layer2) {
|
|
889
823
|
return layer((container$1) => {
|
|
890
824
|
const container1 = layer1.register(container$1);
|
|
891
|
-
|
|
892
|
-
|
|
825
|
+
const container2 = layer2.register(container1);
|
|
826
|
+
return container2;
|
|
827
|
+
});
|
|
893
828
|
}
|
|
894
829
|
/**
|
|
895
830
|
* Utility object containing helper functions for working with layers.
|
|
896
831
|
*/
|
|
897
832
|
const Layer = {
|
|
898
833
|
empty() {
|
|
899
|
-
return layer((container$1) => container$1)
|
|
834
|
+
return layer((container$1) => container$1);
|
|
900
835
|
},
|
|
901
|
-
|
|
902
|
-
return layers.reduce((acc, layer$1) => acc.
|
|
836
|
+
mergeAll(...layers) {
|
|
837
|
+
return layers.reduce((acc, layer$1) => acc.merge(layer$1));
|
|
838
|
+
},
|
|
839
|
+
merge(layer1, layer2) {
|
|
840
|
+
return layer1.merge(layer2);
|
|
903
841
|
}
|
|
904
842
|
};
|
|
905
843
|
|
|
844
|
+
//#endregion
|
|
845
|
+
//#region src/scoped-container.ts
|
|
846
|
+
var ScopedContainer = class ScopedContainer extends Container {
|
|
847
|
+
scope;
|
|
848
|
+
parent;
|
|
849
|
+
children = [];
|
|
850
|
+
constructor(parent, scope) {
|
|
851
|
+
super();
|
|
852
|
+
this.parent = parent;
|
|
853
|
+
this.scope = scope;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Registers a dependency in the scoped container.
|
|
857
|
+
*
|
|
858
|
+
* Overrides the base implementation to return ScopedContainer type
|
|
859
|
+
* for proper method chaining support.
|
|
860
|
+
*/
|
|
861
|
+
register(tag, spec) {
|
|
862
|
+
super.register(tag, spec);
|
|
863
|
+
return this;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Checks if a dependency has been registered in this scope or any parent scope.
|
|
867
|
+
*
|
|
868
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
869
|
+
* Returns true if the dependency has been registered somewhere in the scope hierarchy.
|
|
870
|
+
*/
|
|
871
|
+
has(tag) {
|
|
872
|
+
if (super.has(tag)) return true;
|
|
873
|
+
return this.parent?.has(tag) ?? false;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
877
|
+
*
|
|
878
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
879
|
+
* Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
|
|
880
|
+
*/
|
|
881
|
+
exists(tag) {
|
|
882
|
+
if (super.exists(tag)) return true;
|
|
883
|
+
return this.parent?.exists(tag) ?? false;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
887
|
+
*
|
|
888
|
+
* Resolution strategy:
|
|
889
|
+
* 1. Check cache in current scope
|
|
890
|
+
* 2. Check if factory exists in current scope - if so, create instance here
|
|
891
|
+
* 3. Otherwise, delegate to parent scope
|
|
892
|
+
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
893
|
+
*/
|
|
894
|
+
async get(tag) {
|
|
895
|
+
if (this.factories.has(tag)) return super.get(tag);
|
|
896
|
+
if (this.parent !== null) return this.parent.get(tag);
|
|
897
|
+
throw new UnknownDependencyError(tag);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
901
|
+
*
|
|
902
|
+
* This method ensures proper cleanup order while maintaining reusability:
|
|
903
|
+
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
904
|
+
* 2. Then calls finalizers for dependencies created in this scope
|
|
905
|
+
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
906
|
+
*
|
|
907
|
+
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
908
|
+
* before their dependents.
|
|
909
|
+
*/
|
|
910
|
+
async destroy() {
|
|
911
|
+
if (this.isDestroyed) return;
|
|
912
|
+
const allFailures = [];
|
|
913
|
+
const childDestroyPromises = this.children.map((weakRef) => weakRef.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
|
|
914
|
+
const childResults = await Promise.allSettled(childDestroyPromises);
|
|
915
|
+
const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
916
|
+
allFailures.push(...childFailures);
|
|
917
|
+
try {
|
|
918
|
+
await super.destroy();
|
|
919
|
+
} catch (error) {
|
|
920
|
+
allFailures.push(error);
|
|
921
|
+
} finally {
|
|
922
|
+
this.parent = null;
|
|
923
|
+
}
|
|
924
|
+
if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Creates a new scoped container by merging this container's registrations with another container.
|
|
928
|
+
*
|
|
929
|
+
* This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
|
|
930
|
+
* The resulting scoped container contains all registrations from both containers and becomes a root scope
|
|
931
|
+
* (no parent) with the scope name from this container.
|
|
932
|
+
*
|
|
933
|
+
* @param other - The container to merge with
|
|
934
|
+
* @returns A new ScopedContainer with combined registrations
|
|
935
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
936
|
+
*/
|
|
937
|
+
merge(other) {
|
|
938
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
|
|
939
|
+
const merged = new ScopedContainer(null, this.scope);
|
|
940
|
+
other.copyTo(merged);
|
|
941
|
+
this.copyTo(merged);
|
|
942
|
+
return merged;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Creates a child scoped container.
|
|
946
|
+
*
|
|
947
|
+
* Child containers inherit access to parent dependencies but maintain
|
|
948
|
+
* their own scope for new registrations and instance caching.
|
|
949
|
+
*/
|
|
950
|
+
child(scope) {
|
|
951
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
|
|
952
|
+
const child = new ScopedContainer(this, scope);
|
|
953
|
+
this.children.push(new WeakRef(child));
|
|
954
|
+
return child;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
/**
|
|
958
|
+
* Converts a regular container into a scoped container, copying all registrations.
|
|
959
|
+
*
|
|
960
|
+
* This function creates a new ScopedContainer instance and copies all factory functions
|
|
961
|
+
* and finalizers from the source container. The resulting scoped container becomes a root
|
|
962
|
+
* scope (no parent) with all the same dependency registrations.
|
|
963
|
+
*
|
|
964
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
965
|
+
* The new scoped container starts with an empty instance cache.
|
|
966
|
+
*
|
|
967
|
+
* @param container - The container to convert to a scoped container
|
|
968
|
+
* @param scope - A string or symbol identifier for this scope (used for debugging)
|
|
969
|
+
* @returns A new ScopedContainer instance with all registrations copied from the source container
|
|
970
|
+
* @throws {ContainerDestroyedError} If the source container has been destroyed
|
|
971
|
+
*
|
|
972
|
+
* @example Converting a regular container to scoped
|
|
973
|
+
* ```typescript
|
|
974
|
+
* import { container, scoped } from 'sandly';
|
|
975
|
+
*
|
|
976
|
+
* const appContainer = container()
|
|
977
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
978
|
+
* .register(ConfigService, () => new ConfigService());
|
|
979
|
+
*
|
|
980
|
+
* const scopedAppContainer = scoped(appContainer, 'app');
|
|
981
|
+
*
|
|
982
|
+
* // Create child scopes
|
|
983
|
+
* const requestContainer = scopedAppContainer.child('request');
|
|
984
|
+
* ```
|
|
985
|
+
*
|
|
986
|
+
* @example Copying complex registrations
|
|
987
|
+
* ```typescript
|
|
988
|
+
* const baseContainer = container()
|
|
989
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
990
|
+
* .register(UserService, {
|
|
991
|
+
* factory: async (ctx) => new UserService(await ctx.get(DatabaseService)),
|
|
992
|
+
* finalizer: (service) => service.cleanup()
|
|
993
|
+
* });
|
|
994
|
+
*
|
|
995
|
+
* const scopedContainer = scoped(baseContainer, 'app');
|
|
996
|
+
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
function scoped(container$1, scope) {
|
|
1000
|
+
const emptyScoped = new ScopedContainer(null, scope);
|
|
1001
|
+
const result = emptyScoped.merge(container$1);
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
906
1005
|
//#endregion
|
|
907
1006
|
//#region src/service.ts
|
|
908
1007
|
/**
|
|
@@ -916,10 +1015,9 @@ const Layer = {
|
|
|
916
1015
|
* - No constructor dependencies are needed since they don't have constructors
|
|
917
1016
|
*
|
|
918
1017
|
* @template T - The tag representing the service (ClassTag or ValueTag)
|
|
919
|
-
* @template TParams - Optional parameters for service configuration
|
|
920
1018
|
* @param serviceClass - The tag (ClassTag or ValueTag)
|
|
921
|
-
* @param factory - Factory function for service instantiation with container
|
|
922
|
-
* @returns
|
|
1019
|
+
* @param factory - Factory function for service instantiation with container
|
|
1020
|
+
* @returns The service layer
|
|
923
1021
|
*
|
|
924
1022
|
* @example Simple service without dependencies
|
|
925
1023
|
* ```typescript
|
|
@@ -944,40 +1042,40 @@ const Layer = {
|
|
|
944
1042
|
* getUsers() { return this.db.query(); }
|
|
945
1043
|
* }
|
|
946
1044
|
*
|
|
947
|
-
* const userService = service(UserService, async (
|
|
948
|
-
* new UserService(await
|
|
1045
|
+
* const userService = service(UserService, async (ctx) =>
|
|
1046
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
949
1047
|
* );
|
|
950
1048
|
* ```
|
|
1049
|
+
*/
|
|
1050
|
+
function service(serviceClass, spec) {
|
|
1051
|
+
return layer((container$1) => {
|
|
1052
|
+
return container$1.register(serviceClass, spec);
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/value.ts
|
|
1058
|
+
/**
|
|
1059
|
+
* Creates a layer that provides a constant value for a given tag.
|
|
951
1060
|
*
|
|
952
|
-
* @
|
|
1061
|
+
* @param tag - The value tag to provide
|
|
1062
|
+
* @param constantValue - The constant value to provide
|
|
1063
|
+
* @returns A layer with no dependencies that provides the constant value
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
953
1066
|
* ```typescript
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
* super();
|
|
957
|
-
* }
|
|
958
|
-
* }
|
|
1067
|
+
* const ApiKey = Tag.of('ApiKey')<string>();
|
|
1068
|
+
* const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
|
|
959
1069
|
*
|
|
960
|
-
* const
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
* );
|
|
1070
|
+
* const apiKey = value(ApiKey, 'my-secret-key');
|
|
1071
|
+
* const dbUrl = value(DatabaseUrl, 'postgresql://localhost:5432/myapp');
|
|
1072
|
+
*
|
|
1073
|
+
* const config = Layer.merge(apiKey, dbUrl);
|
|
964
1074
|
* ```
|
|
965
1075
|
*/
|
|
966
|
-
function
|
|
967
|
-
|
|
968
|
-
const serviceLayer = layer((container$1) => {
|
|
969
|
-
return container$1.register(serviceClass, (c) => factory(c, params));
|
|
970
|
-
})();
|
|
971
|
-
const serviceImpl = {
|
|
972
|
-
serviceClass,
|
|
973
|
-
register: serviceLayer.register,
|
|
974
|
-
to: serviceLayer.to,
|
|
975
|
-
and: serviceLayer.and
|
|
976
|
-
};
|
|
977
|
-
return serviceImpl;
|
|
978
|
-
};
|
|
979
|
-
return serviceFactory;
|
|
1076
|
+
function value(tag, constantValue) {
|
|
1077
|
+
return layer((container$1) => container$1.register(tag, () => constantValue));
|
|
980
1078
|
}
|
|
981
1079
|
|
|
982
1080
|
//#endregion
|
|
983
|
-
export { Layer, Tag, container, layer,
|
|
1081
|
+
export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, Layer, ScopedContainer, Tag, UnknownDependencyError, container, layer, scoped, service, value };
|