sandly 0.5.0 → 0.5.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/dist/index.d.ts +161 -137
- package/dist/index.js +131 -169
- package/package.json +75 -74
package/dist/index.d.ts
CHANGED
|
@@ -407,21 +407,21 @@ type Factory<T, TReg extends AnyTag> = (ctx: ResolutionContext<TReg>) => Promise
|
|
|
407
407
|
*
|
|
408
408
|
* @example Synchronous finalizer
|
|
409
409
|
* ```typescript
|
|
410
|
-
* const
|
|
410
|
+
* const cleanup: Finalizer<FileHandle> = (fileHandle) => {
|
|
411
411
|
* fileHandle.close();
|
|
412
412
|
* };
|
|
413
413
|
* ```
|
|
414
414
|
*
|
|
415
415
|
* @example Asynchronous finalizer
|
|
416
416
|
* ```typescript
|
|
417
|
-
* const
|
|
417
|
+
* const cleanup: Finalizer<DatabaseConnection> = async (connection) => {
|
|
418
418
|
* await connection.disconnect();
|
|
419
419
|
* };
|
|
420
420
|
* ```
|
|
421
421
|
*
|
|
422
422
|
* @example Resilient finalizer
|
|
423
423
|
* ```typescript
|
|
424
|
-
* const
|
|
424
|
+
* const cleanup: Finalizer<HttpServer> = async (server) => {
|
|
425
425
|
* try {
|
|
426
426
|
* await server.close();
|
|
427
427
|
* } catch (error) {
|
|
@@ -437,38 +437,97 @@ type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
|
437
437
|
/**
|
|
438
438
|
* Type representing a complete dependency lifecycle with both factory and finalizer.
|
|
439
439
|
*
|
|
440
|
-
* This
|
|
440
|
+
* This interface is used when registering dependencies that need cleanup. Instead of
|
|
441
441
|
* passing separate factory and finalizer parameters, you can pass an object
|
|
442
442
|
* containing both.
|
|
443
443
|
*
|
|
444
|
-
*
|
|
444
|
+
* Since this is an interface, you can also implement it as a class for better
|
|
445
|
+
* organization and reuse. This is particularly useful when you have complex
|
|
446
|
+
* lifecycle logic or want to share lifecycle definitions across multiple services.
|
|
447
|
+
*
|
|
448
|
+
* @template T - The instance type
|
|
445
449
|
* @template TReg - Union type of all dependencies available in the container
|
|
446
450
|
*
|
|
447
|
-
* @example Using DependencyLifecycle
|
|
451
|
+
* @example Using DependencyLifecycle as an object
|
|
448
452
|
* ```typescript
|
|
453
|
+
* import { Container, Tag } from 'sandly';
|
|
454
|
+
*
|
|
449
455
|
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
450
456
|
* async connect() { return; }
|
|
451
457
|
* async disconnect() { return; }
|
|
452
458
|
* }
|
|
453
459
|
*
|
|
454
|
-
* const lifecycle: DependencyLifecycle<
|
|
455
|
-
*
|
|
460
|
+
* const lifecycle: DependencyLifecycle<DatabaseConnection, never> = {
|
|
461
|
+
* create: async () => {
|
|
456
462
|
* const conn = new DatabaseConnection();
|
|
457
463
|
* await conn.connect();
|
|
458
464
|
* return conn;
|
|
459
465
|
* },
|
|
460
|
-
*
|
|
466
|
+
* cleanup: async (conn) => {
|
|
461
467
|
* await conn.disconnect();
|
|
462
468
|
* }
|
|
463
469
|
* };
|
|
464
470
|
*
|
|
465
471
|
* Container.empty().register(DatabaseConnection, lifecycle);
|
|
466
472
|
* ```
|
|
473
|
+
*
|
|
474
|
+
* @example Implementing DependencyLifecycle as a class with dependencies
|
|
475
|
+
* ```typescript
|
|
476
|
+
* import { Container, Tag, type ResolutionContext } from 'sandly';
|
|
477
|
+
*
|
|
478
|
+
* class Logger extends Tag.Service('Logger') {
|
|
479
|
+
* log(message: string) { console.log(message); }
|
|
480
|
+
* }
|
|
481
|
+
*
|
|
482
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
483
|
+
* constructor(private logger: Logger, private url: string) { super(); }
|
|
484
|
+
* async connect() { this.logger.log('Connected'); }
|
|
485
|
+
* async disconnect() { this.logger.log('Disconnected'); }
|
|
486
|
+
* }
|
|
487
|
+
*
|
|
488
|
+
* class DatabaseLifecycle implements DependencyLifecycle<DatabaseConnection, typeof Logger> {
|
|
489
|
+
* constructor(private url: string) {}
|
|
490
|
+
*
|
|
491
|
+
* async create(ctx: ResolutionContext<typeof Logger>): Promise<DatabaseConnection> {
|
|
492
|
+
* const logger = await ctx.resolve(Logger);
|
|
493
|
+
* const conn = new DatabaseConnection(logger, this.url);
|
|
494
|
+
* await conn.connect();
|
|
495
|
+
* return conn;
|
|
496
|
+
* }
|
|
497
|
+
*
|
|
498
|
+
* async cleanup(conn: DatabaseConnection): Promise<void> {
|
|
499
|
+
* await conn.disconnect();
|
|
500
|
+
* }
|
|
501
|
+
* }
|
|
502
|
+
*
|
|
503
|
+
* const container = Container.empty()
|
|
504
|
+
* .register(Logger, () => new Logger())
|
|
505
|
+
* .register(DatabaseConnection, new DatabaseLifecycle('postgresql://localhost:5432'));
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* @example Class with only factory (no cleanup)
|
|
509
|
+
* ```typescript
|
|
510
|
+
* import { Container, Tag } from 'sandly';
|
|
511
|
+
*
|
|
512
|
+
* class SimpleService extends Tag.Service('SimpleService') {}
|
|
513
|
+
*
|
|
514
|
+
* class SimpleServiceLifecycle implements DependencyLifecycle<SimpleService, never> {
|
|
515
|
+
* create(): SimpleService {
|
|
516
|
+
* return new SimpleService();
|
|
517
|
+
* }
|
|
518
|
+
* // cleanup is optional, so it can be omitted
|
|
519
|
+
* }
|
|
520
|
+
*
|
|
521
|
+
* const container = Container.empty().register(
|
|
522
|
+
* SimpleService,
|
|
523
|
+
* new SimpleServiceLifecycle()
|
|
524
|
+
* );
|
|
525
|
+
* ```
|
|
467
526
|
*/
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
527
|
+
interface DependencyLifecycle<T, TReg extends AnyTag> {
|
|
528
|
+
create: Factory<T, TReg>;
|
|
529
|
+
cleanup?: Finalizer<T>;
|
|
530
|
+
}
|
|
472
531
|
/**
|
|
473
532
|
* Union type representing all valid dependency registration specifications.
|
|
474
533
|
*
|
|
@@ -490,8 +549,8 @@ type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
|
490
549
|
* @example Lifecycle registration
|
|
491
550
|
* ```typescript
|
|
492
551
|
* const spec: DependencySpec<typeof DatabaseConnection, never> = {
|
|
493
|
-
*
|
|
494
|
-
*
|
|
552
|
+
* create: () => new DatabaseConnection(),
|
|
553
|
+
* cleanup: (conn) => conn.close()
|
|
495
554
|
* };
|
|
496
555
|
*
|
|
497
556
|
* Container.empty().register(DatabaseConnection, spec);
|
|
@@ -522,7 +581,6 @@ interface IContainer<TReg extends AnyTag = never> {
|
|
|
522
581
|
exists(tag: AnyTag): boolean;
|
|
523
582
|
resolve: <T extends TReg>(tag: T) => Promise<TagType<T>>;
|
|
524
583
|
resolveAll: <const T extends readonly TReg[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
525
|
-
merge<TTarget extends AnyTag>(other: IContainer<TTarget>): IContainer<TReg | TTarget>;
|
|
526
584
|
destroy(): Promise<void>;
|
|
527
585
|
}
|
|
528
586
|
/**
|
|
@@ -736,8 +794,8 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
736
794
|
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
737
795
|
* requests for the same dependency correctly.
|
|
738
796
|
*
|
|
739
|
-
* The method performs circular dependency detection
|
|
740
|
-
* the resolution
|
|
797
|
+
* The method performs circular dependency detection by tracking the resolution chain
|
|
798
|
+
* through the resolution context.
|
|
741
799
|
*
|
|
742
800
|
* @template T - The dependency tag type (must be registered in this container)
|
|
743
801
|
* @param tag - The dependency tag to retrieve
|
|
@@ -780,6 +838,12 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
780
838
|
* ```
|
|
781
839
|
*/
|
|
782
840
|
resolve<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
841
|
+
/**
|
|
842
|
+
* Internal resolution method that tracks the dependency chain for circular dependency detection.
|
|
843
|
+
* Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
|
|
844
|
+
* @internal
|
|
845
|
+
*/
|
|
846
|
+
protected resolveInternal<T extends TReg>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
783
847
|
/**
|
|
784
848
|
* Resolves multiple dependencies concurrently using Promise.all.
|
|
785
849
|
*
|
|
@@ -821,51 +885,16 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
821
885
|
* ```
|
|
822
886
|
*/
|
|
823
887
|
resolveAll<const T extends readonly TReg[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
824
|
-
/**
|
|
825
|
-
* Copies all registrations from this container to a target container.
|
|
826
|
-
*
|
|
827
|
-
* @internal
|
|
828
|
-
* @param target - The container to copy registrations to
|
|
829
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
830
|
-
*/
|
|
831
|
-
copyTo<TTarget extends AnyTag>(target: Container<TTarget>): void;
|
|
832
|
-
/**
|
|
833
|
-
* Creates a new container by merging this container's registrations with another container.
|
|
834
|
-
*
|
|
835
|
-
* This method creates a new container that contains all registrations from both containers.
|
|
836
|
-
* If there are conflicts (same dependency registered in both containers), this
|
|
837
|
-
* container's registration will take precedence.
|
|
838
|
-
*
|
|
839
|
-
* **Important**: Only the registrations are copied, not any cached instances.
|
|
840
|
-
* The new container starts with an empty instance cache.
|
|
841
|
-
*
|
|
842
|
-
* @param other - The container to merge with
|
|
843
|
-
* @returns A new container with combined registrations
|
|
844
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
845
|
-
*
|
|
846
|
-
* @example Merging containers
|
|
847
|
-
* ```typescript
|
|
848
|
-
* const container1 = Container.empty()
|
|
849
|
-
* .register(DatabaseService, () => new DatabaseService());
|
|
850
|
-
*
|
|
851
|
-
* const container2 = Container.empty()
|
|
852
|
-
* .register(UserService, () => new UserService());
|
|
853
|
-
*
|
|
854
|
-
* const merged = container1.merge(container2);
|
|
855
|
-
* // merged has both DatabaseService and UserService
|
|
856
|
-
* ```
|
|
857
|
-
*/
|
|
858
|
-
merge<TTarget extends AnyTag>(other: Container<TTarget>): Container<TReg | TTarget>;
|
|
859
888
|
/**
|
|
860
889
|
* Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
|
|
861
890
|
*
|
|
862
891
|
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
863
|
-
* Any subsequent calls to register(), get(), or destroy() will throw a
|
|
892
|
+
* Any subsequent calls to register(), get(), or destroy() will throw a DependencyFinalizationError.
|
|
864
893
|
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
865
894
|
*
|
|
866
895
|
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
867
896
|
* for maximum cleanup performance.
|
|
868
|
-
* If any finalizers fail, all errors are collected and a
|
|
897
|
+
* If any finalizers fail, all errors are collected and a DependencyFinalizationError
|
|
869
898
|
* is thrown containing details of all failures.
|
|
870
899
|
*
|
|
871
900
|
* **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
|
|
@@ -946,35 +975,34 @@ type ErrorDump = {
|
|
|
946
975
|
cause?: unknown;
|
|
947
976
|
};
|
|
948
977
|
};
|
|
949
|
-
declare class BaseError extends Error {
|
|
950
|
-
detail: Record<string, unknown> | undefined;
|
|
951
|
-
constructor(message: string, {
|
|
952
|
-
cause,
|
|
953
|
-
detail
|
|
954
|
-
}?: ErrorProps);
|
|
955
|
-
static ensure(error: unknown): BaseError;
|
|
956
|
-
dump(): ErrorDump;
|
|
957
|
-
dumps(): string;
|
|
958
|
-
}
|
|
959
978
|
/**
|
|
960
|
-
* Base error class for all
|
|
979
|
+
* Base error class for all library errors.
|
|
961
980
|
*
|
|
962
|
-
* This extends the
|
|
963
|
-
* and structured error information across the
|
|
981
|
+
* This extends the native Error class to provide consistent error handling
|
|
982
|
+
* and structured error information across the library.
|
|
964
983
|
*
|
|
965
|
-
* @example Catching
|
|
984
|
+
* @example Catching library errors
|
|
966
985
|
* ```typescript
|
|
967
986
|
* try {
|
|
968
987
|
* await container.resolve(SomeService);
|
|
969
988
|
* } catch (error) {
|
|
970
|
-
* if (error instanceof
|
|
989
|
+
* if (error instanceof SandlyError) {
|
|
971
990
|
* console.error('DI Error:', error.message);
|
|
972
991
|
* console.error('Details:', error.detail);
|
|
973
992
|
* }
|
|
974
993
|
* }
|
|
975
994
|
* ```
|
|
976
995
|
*/
|
|
977
|
-
declare class
|
|
996
|
+
declare class SandlyError extends Error {
|
|
997
|
+
detail: Record<string, unknown> | undefined;
|
|
998
|
+
constructor(message: string, {
|
|
999
|
+
cause,
|
|
1000
|
+
detail
|
|
1001
|
+
}?: ErrorProps);
|
|
1002
|
+
static ensure(error: unknown): SandlyError;
|
|
1003
|
+
dump(): ErrorDump;
|
|
1004
|
+
dumps(): string;
|
|
1005
|
+
}
|
|
978
1006
|
/**
|
|
979
1007
|
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
980
1008
|
*
|
|
@@ -982,7 +1010,7 @@ declare class ContainerError extends BaseError {}
|
|
|
982
1010
|
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
983
1011
|
* by existing dependencies.
|
|
984
1012
|
*/
|
|
985
|
-
declare class DependencyAlreadyInstantiatedError extends
|
|
1013
|
+
declare class DependencyAlreadyInstantiatedError extends SandlyError {}
|
|
986
1014
|
/**
|
|
987
1015
|
* Error thrown when attempting to use a container that has been destroyed.
|
|
988
1016
|
*
|
|
@@ -990,7 +1018,7 @@ declare class DependencyAlreadyInstantiatedError extends ContainerError {}
|
|
|
990
1018
|
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
991
1019
|
* is being used after it has been destroyed.
|
|
992
1020
|
*/
|
|
993
|
-
declare class ContainerDestroyedError extends
|
|
1021
|
+
declare class ContainerDestroyedError extends SandlyError {}
|
|
994
1022
|
/**
|
|
995
1023
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
996
1024
|
*
|
|
@@ -1011,7 +1039,7 @@ declare class ContainerDestroyedError extends ContainerError {}
|
|
|
1011
1039
|
* }
|
|
1012
1040
|
* ```
|
|
1013
1041
|
*/
|
|
1014
|
-
declare class UnknownDependencyError extends
|
|
1042
|
+
declare class UnknownDependencyError extends SandlyError {
|
|
1015
1043
|
/**
|
|
1016
1044
|
* @internal
|
|
1017
1045
|
* Creates an UnknownDependencyError for the given tag.
|
|
@@ -1050,7 +1078,7 @@ declare class UnknownDependencyError extends ContainerError {
|
|
|
1050
1078
|
* }
|
|
1051
1079
|
* ```
|
|
1052
1080
|
*/
|
|
1053
|
-
declare class CircularDependencyError extends
|
|
1081
|
+
declare class CircularDependencyError extends SandlyError {
|
|
1054
1082
|
/**
|
|
1055
1083
|
* @internal
|
|
1056
1084
|
* Creates a CircularDependencyError with the dependency chain information.
|
|
@@ -1066,6 +1094,9 @@ declare class CircularDependencyError extends ContainerError {
|
|
|
1066
1094
|
* This wraps the original error with additional context about which dependency
|
|
1067
1095
|
* failed to be created. The original error is preserved as the `cause` property.
|
|
1068
1096
|
*
|
|
1097
|
+
* When dependencies are nested (A depends on B depends on C), and C's factory throws,
|
|
1098
|
+
* you get nested DependencyCreationErrors. Use `getRootCause()` to get the original error.
|
|
1099
|
+
*
|
|
1069
1100
|
* @example Factory throwing error
|
|
1070
1101
|
* ```typescript
|
|
1071
1102
|
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
@@ -1083,8 +1114,22 @@ declare class CircularDependencyError extends ContainerError {
|
|
|
1083
1114
|
* }
|
|
1084
1115
|
* }
|
|
1085
1116
|
* ```
|
|
1117
|
+
*
|
|
1118
|
+
* @example Getting root cause from nested errors
|
|
1119
|
+
* ```typescript
|
|
1120
|
+
* // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
|
|
1121
|
+
* try {
|
|
1122
|
+
* await container.resolve(ServiceA);
|
|
1123
|
+
* } catch (error) {
|
|
1124
|
+
* if (error instanceof DependencyCreationError) {
|
|
1125
|
+
* console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
|
|
1126
|
+
* const rootCause = error.getRootCause();
|
|
1127
|
+
* console.error('Root cause:', rootCause); // Original error from ServiceC
|
|
1128
|
+
* }
|
|
1129
|
+
* }
|
|
1130
|
+
* ```
|
|
1086
1131
|
*/
|
|
1087
|
-
declare class DependencyCreationError extends
|
|
1132
|
+
declare class DependencyCreationError extends SandlyError {
|
|
1088
1133
|
/**
|
|
1089
1134
|
* @internal
|
|
1090
1135
|
* Creates a DependencyCreationError wrapping the original factory error.
|
|
@@ -1093,6 +1138,27 @@ declare class DependencyCreationError extends ContainerError {
|
|
|
1093
1138
|
* @param error - The original error thrown by the factory function
|
|
1094
1139
|
*/
|
|
1095
1140
|
constructor(tag: AnyTag, error: unknown);
|
|
1141
|
+
/**
|
|
1142
|
+
* Traverses the error chain to find the root cause error.
|
|
1143
|
+
*
|
|
1144
|
+
* When dependencies are nested, each level wraps the error in a DependencyCreationError.
|
|
1145
|
+
* This method unwraps all the layers to get to the original error that started the failure.
|
|
1146
|
+
*
|
|
1147
|
+
* @returns The root cause error (not a DependencyCreationError unless that's the only error)
|
|
1148
|
+
*
|
|
1149
|
+
* @example
|
|
1150
|
+
* ```typescript
|
|
1151
|
+
* try {
|
|
1152
|
+
* await container.resolve(UserService);
|
|
1153
|
+
* } catch (error) {
|
|
1154
|
+
* if (error instanceof DependencyCreationError) {
|
|
1155
|
+
* const rootCause = error.getRootCause();
|
|
1156
|
+
* console.error('Root cause:', rootCause);
|
|
1157
|
+
* }
|
|
1158
|
+
* }
|
|
1159
|
+
* ```
|
|
1160
|
+
*/
|
|
1161
|
+
getRootCause(): unknown;
|
|
1096
1162
|
}
|
|
1097
1163
|
/**
|
|
1098
1164
|
* Error thrown when one or more finalizers fail during container destruction.
|
|
@@ -1113,7 +1179,8 @@ declare class DependencyCreationError extends ContainerError {
|
|
|
1113
1179
|
* }
|
|
1114
1180
|
* ```
|
|
1115
1181
|
*/
|
|
1116
|
-
declare class DependencyFinalizationError extends
|
|
1182
|
+
declare class DependencyFinalizationError extends SandlyError {
|
|
1183
|
+
private readonly errors;
|
|
1117
1184
|
/**
|
|
1118
1185
|
* @internal
|
|
1119
1186
|
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
@@ -1121,6 +1188,13 @@ declare class DependencyFinalizationError extends ContainerError {
|
|
|
1121
1188
|
* @param errors - Array of errors thrown by individual finalizers
|
|
1122
1189
|
*/
|
|
1123
1190
|
constructor(errors: unknown[]);
|
|
1191
|
+
/**
|
|
1192
|
+
* Returns the root causes of the errors that occurred during finalization.
|
|
1193
|
+
*
|
|
1194
|
+
* @returns An array of the errors that occurred during finalization.
|
|
1195
|
+
* You can expect at least one error in the array.
|
|
1196
|
+
*/
|
|
1197
|
+
getRootCauses(): unknown[];
|
|
1124
1198
|
}
|
|
1125
1199
|
//#endregion
|
|
1126
1200
|
//#region src/layer.d.ts
|
|
@@ -1547,6 +1621,11 @@ declare class ScopedContainer<TReg extends AnyTag> extends Container<TReg> {
|
|
|
1547
1621
|
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
1548
1622
|
*/
|
|
1549
1623
|
resolve<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
1624
|
+
/**
|
|
1625
|
+
* Internal resolution with delegation logic for scoped containers.
|
|
1626
|
+
* @internal
|
|
1627
|
+
*/
|
|
1628
|
+
protected resolveInternal<T extends TReg>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
1550
1629
|
/**
|
|
1551
1630
|
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
1552
1631
|
*
|
|
@@ -1559,18 +1638,6 @@ declare class ScopedContainer<TReg extends AnyTag> extends Container<TReg> {
|
|
|
1559
1638
|
* before their dependents.
|
|
1560
1639
|
*/
|
|
1561
1640
|
destroy(): Promise<void>;
|
|
1562
|
-
/**
|
|
1563
|
-
* Creates a new scoped container by merging this container's registrations with another container.
|
|
1564
|
-
*
|
|
1565
|
-
* This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
|
|
1566
|
-
* The resulting scoped container contains all registrations from both containers and becomes a root scope
|
|
1567
|
-
* (no parent) with the scope name from this container.
|
|
1568
|
-
*
|
|
1569
|
-
* @param other - The container to merge with
|
|
1570
|
-
* @returns A new ScopedContainer with combined registrations
|
|
1571
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
1572
|
-
*/
|
|
1573
|
-
merge<TTarget extends AnyTag>(other: Container<TTarget>): ScopedContainer<TReg | TTarget>;
|
|
1574
1641
|
/**
|
|
1575
1642
|
* Creates a child scoped container.
|
|
1576
1643
|
*
|
|
@@ -1579,49 +1646,6 @@ declare class ScopedContainer<TReg extends AnyTag> extends Container<TReg> {
|
|
|
1579
1646
|
*/
|
|
1580
1647
|
child(scope: Scope): ScopedContainer<TReg>;
|
|
1581
1648
|
}
|
|
1582
|
-
/**
|
|
1583
|
-
* Converts a regular container into a scoped container, copying all registrations.
|
|
1584
|
-
*
|
|
1585
|
-
* This function creates a new ScopedContainer instance and copies all factory functions
|
|
1586
|
-
* and finalizers from the source container. The resulting scoped container becomes a root
|
|
1587
|
-
* scope (no parent) with all the same dependency registrations.
|
|
1588
|
-
*
|
|
1589
|
-
* **Important**: Only the registrations are copied, not any cached instances.
|
|
1590
|
-
* The new scoped container starts with an empty instance cache.
|
|
1591
|
-
*
|
|
1592
|
-
* @param container - The container to convert to a scoped container
|
|
1593
|
-
* @param scope - A string or symbol identifier for this scope (used for debugging)
|
|
1594
|
-
* @returns A new ScopedContainer instance with all registrations copied from the source container
|
|
1595
|
-
* @throws {ContainerDestroyedError} If the source container has been destroyed
|
|
1596
|
-
*
|
|
1597
|
-
* @example Converting a regular container to scoped
|
|
1598
|
-
* ```typescript
|
|
1599
|
-
* import { container, scoped } from 'sandly';
|
|
1600
|
-
*
|
|
1601
|
-
* const appContainer = Container.empty()
|
|
1602
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
1603
|
-
* .register(ConfigService, () => new ConfigService());
|
|
1604
|
-
*
|
|
1605
|
-
* const scopedAppContainer = scoped(appContainer, 'app');
|
|
1606
|
-
*
|
|
1607
|
-
* // Create child scopes
|
|
1608
|
-
* const requestContainer = scopedAppContainer.child('request');
|
|
1609
|
-
* ```
|
|
1610
|
-
*
|
|
1611
|
-
* @example Copying complex registrations
|
|
1612
|
-
* ```typescript
|
|
1613
|
-
* const baseContainer = Container.empty()
|
|
1614
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
1615
|
-
* .register(UserService, {
|
|
1616
|
-
* factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
|
|
1617
|
-
* finalizer: (service) => service.cleanup()
|
|
1618
|
-
* });
|
|
1619
|
-
*
|
|
1620
|
-
* const scopedContainer = scoped(baseContainer, 'app');
|
|
1621
|
-
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
1622
|
-
* ```
|
|
1623
|
-
*/
|
|
1624
|
-
declare function scoped<TReg extends AnyTag>(container: Container<TReg>, scope: Scope): ScopedContainer<TReg>;
|
|
1625
1649
|
//#endregion
|
|
1626
1650
|
//#region src/service.d.ts
|
|
1627
1651
|
/**
|
|
@@ -1641,7 +1665,7 @@ type ConstructorParams<T extends ServiceTag<TagId, unknown>> = T extends (new (.
|
|
|
1641
1665
|
*/
|
|
1642
1666
|
type ExtractConstructorDeps<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1643
1667
|
readonly [ServiceTagIdKey]?: infer Id;
|
|
1644
|
-
} ? Id extends TagId ? ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
|
|
1668
|
+
} ? Id extends TagId ? T[K] extends (new (...args: unknown[]) => infer Instance) ? ServiceTag<Id, Instance> : ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
|
|
1645
1669
|
/**
|
|
1646
1670
|
* Produces an ordered tuple of constructor parameters
|
|
1647
1671
|
* where dependency parameters are replaced with their tag types,
|
|
@@ -1650,7 +1674,7 @@ type ExtractConstructorDeps<T extends readonly unknown[]> = T extends readonly [
|
|
|
1650
1674
|
*/
|
|
1651
1675
|
type InferConstructorDepsTuple<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1652
1676
|
readonly [ServiceTagIdKey]?: infer Id;
|
|
1653
|
-
} ? Id extends TagId ? ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? T[K] : ExtractInjectTag<T[K]> };
|
|
1677
|
+
} ? Id extends TagId ? T[K] extends (new (...args: unknown[]) => infer Instance) ? ServiceTag<Id, Instance> : ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? T[K] : ExtractInjectTag<T[K]> };
|
|
1654
1678
|
/**
|
|
1655
1679
|
* Union of all dependency tags a ServiceTag constructor requires.
|
|
1656
1680
|
* Filters out non‑DI parameters.
|
|
@@ -1711,7 +1735,7 @@ declare function service<T extends ServiceTag<TagId, unknown>>(tag: T, spec: Dep
|
|
|
1711
1735
|
*/
|
|
1712
1736
|
type AutoServiceSpec<T extends ServiceTag<TagId, unknown>> = ServiceDepsTuple<T> | {
|
|
1713
1737
|
dependencies: ServiceDepsTuple<T>;
|
|
1714
|
-
|
|
1738
|
+
cleanup?: Finalizer<TagType<T>>;
|
|
1715
1739
|
};
|
|
1716
1740
|
/**
|
|
1717
1741
|
* Creates a service layer with automatic dependency injection by inferring constructor parameters.
|
|
@@ -1806,7 +1830,7 @@ type AutoServiceSpec<T extends ServiceTag<TagId, unknown>> = ServiceDepsTuple<T>
|
|
|
1806
1830
|
* DatabaseService,
|
|
1807
1831
|
* {
|
|
1808
1832
|
* dependencies: ['postgresql://localhost:5432/mydb'],
|
|
1809
|
-
*
|
|
1833
|
+
* cleanup: (service) => service.disconnect() // Finalizer for cleanup
|
|
1810
1834
|
* }
|
|
1811
1835
|
* );
|
|
1812
1836
|
* ```
|
|
@@ -1834,4 +1858,4 @@ declare function autoService<T extends ServiceTag<TagId, unknown>>(tag: T, spec:
|
|
|
1834
1858
|
*/
|
|
1835
1859
|
declare function value<T extends ValueTag<TagId, unknown>>(tag: T, constantValue: TagType<T>): Layer<never, T>;
|
|
1836
1860
|
//#endregion
|
|
1837
|
-
export { type AnyLayer, type AnyTag, CircularDependencyError, Container, ContainerDestroyedError,
|
|
1861
|
+
export { type AnyLayer, type AnyTag, CircularDependencyError, Container, ContainerDestroyedError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, type DependencyLifecycle, type DependencySpec, type Factory, type Finalizer, type IContainer, type Inject, InjectSource, Layer, type PromiseOrValue, type ResolutionContext, SandlyError, type Scope, ScopedContainer, type ServiceDependencies, type ServiceDepsTuple, type ServiceTag, Tag, type TagType, UnknownDependencyError, type ValueTag, autoService, layer, service, value };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
|
|
3
1
|
//#region src/utils/object.ts
|
|
4
2
|
function hasKey(obj, key) {
|
|
5
3
|
return obj !== void 0 && obj !== null && (typeof obj === "object" || typeof obj === "function") && key in obj;
|
|
@@ -75,7 +73,25 @@ const InjectSource = "sandly/InjectSource";
|
|
|
75
73
|
|
|
76
74
|
//#endregion
|
|
77
75
|
//#region src/errors.ts
|
|
78
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Base error class for all library errors.
|
|
78
|
+
*
|
|
79
|
+
* This extends the native Error class to provide consistent error handling
|
|
80
|
+
* and structured error information across the library.
|
|
81
|
+
*
|
|
82
|
+
* @example Catching library errors
|
|
83
|
+
* ```typescript
|
|
84
|
+
* try {
|
|
85
|
+
* await container.resolve(SomeService);
|
|
86
|
+
* } catch (error) {
|
|
87
|
+
* if (error instanceof SandlyError) {
|
|
88
|
+
* console.error('DI Error:', error.message);
|
|
89
|
+
* console.error('Details:', error.detail);
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
var SandlyError = class SandlyError extends Error {
|
|
79
95
|
detail;
|
|
80
96
|
constructor(message, { cause, detail } = {}) {
|
|
81
97
|
super(message, { cause });
|
|
@@ -84,10 +100,10 @@ var BaseError = class BaseError extends Error {
|
|
|
84
100
|
if (cause instanceof Error && cause.stack !== void 0) this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
|
|
85
101
|
}
|
|
86
102
|
static ensure(error) {
|
|
87
|
-
return error instanceof
|
|
103
|
+
return error instanceof SandlyError ? error : new SandlyError("An unknown error occurred", { cause: error });
|
|
88
104
|
}
|
|
89
105
|
dump() {
|
|
90
|
-
const cause = this.cause instanceof
|
|
106
|
+
const cause = this.cause instanceof SandlyError ? this.cause.dump().error : this.cause;
|
|
91
107
|
const result = {
|
|
92
108
|
name: this.name,
|
|
93
109
|
message: this.message,
|
|
@@ -106,32 +122,13 @@ var BaseError = class BaseError extends Error {
|
|
|
106
122
|
}
|
|
107
123
|
};
|
|
108
124
|
/**
|
|
109
|
-
* Base error class for all dependency container related errors.
|
|
110
|
-
*
|
|
111
|
-
* This extends the framework's BaseError to provide consistent error handling
|
|
112
|
-
* and structured error information across the dependency injection system.
|
|
113
|
-
*
|
|
114
|
-
* @example Catching DI errors
|
|
115
|
-
* ```typescript
|
|
116
|
-
* try {
|
|
117
|
-
* await container.resolve(SomeService);
|
|
118
|
-
* } catch (error) {
|
|
119
|
-
* if (error instanceof ContainerError) {
|
|
120
|
-
* console.error('DI Error:', error.message);
|
|
121
|
-
* console.error('Details:', error.detail);
|
|
122
|
-
* }
|
|
123
|
-
* }
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
var ContainerError = class extends BaseError {};
|
|
127
|
-
/**
|
|
128
125
|
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
129
126
|
*
|
|
130
127
|
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
131
128
|
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
132
129
|
* by existing dependencies.
|
|
133
130
|
*/
|
|
134
|
-
var DependencyAlreadyInstantiatedError = class extends
|
|
131
|
+
var DependencyAlreadyInstantiatedError = class extends SandlyError {};
|
|
135
132
|
/**
|
|
136
133
|
* Error thrown when attempting to use a container that has been destroyed.
|
|
137
134
|
*
|
|
@@ -139,7 +136,7 @@ var DependencyAlreadyInstantiatedError = class extends ContainerError {};
|
|
|
139
136
|
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
140
137
|
* is being used after it has been destroyed.
|
|
141
138
|
*/
|
|
142
|
-
var ContainerDestroyedError = class extends
|
|
139
|
+
var ContainerDestroyedError = class extends SandlyError {};
|
|
143
140
|
/**
|
|
144
141
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
145
142
|
*
|
|
@@ -160,7 +157,7 @@ var ContainerDestroyedError = class extends ContainerError {};
|
|
|
160
157
|
* }
|
|
161
158
|
* ```
|
|
162
159
|
*/
|
|
163
|
-
var UnknownDependencyError = class extends
|
|
160
|
+
var UnknownDependencyError = class extends SandlyError {
|
|
164
161
|
/**
|
|
165
162
|
* @internal
|
|
166
163
|
* Creates an UnknownDependencyError for the given tag.
|
|
@@ -201,7 +198,7 @@ var UnknownDependencyError = class extends ContainerError {
|
|
|
201
198
|
* }
|
|
202
199
|
* ```
|
|
203
200
|
*/
|
|
204
|
-
var CircularDependencyError = class extends
|
|
201
|
+
var CircularDependencyError = class extends SandlyError {
|
|
205
202
|
/**
|
|
206
203
|
* @internal
|
|
207
204
|
* Creates a CircularDependencyError with the dependency chain information.
|
|
@@ -223,6 +220,9 @@ var CircularDependencyError = class extends ContainerError {
|
|
|
223
220
|
* This wraps the original error with additional context about which dependency
|
|
224
221
|
* failed to be created. The original error is preserved as the `cause` property.
|
|
225
222
|
*
|
|
223
|
+
* When dependencies are nested (A depends on B depends on C), and C's factory throws,
|
|
224
|
+
* you get nested DependencyCreationErrors. Use `getRootCause()` to get the original error.
|
|
225
|
+
*
|
|
226
226
|
* @example Factory throwing error
|
|
227
227
|
* ```typescript
|
|
228
228
|
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
@@ -240,8 +240,22 @@ var CircularDependencyError = class extends ContainerError {
|
|
|
240
240
|
* }
|
|
241
241
|
* }
|
|
242
242
|
* ```
|
|
243
|
+
*
|
|
244
|
+
* @example Getting root cause from nested errors
|
|
245
|
+
* ```typescript
|
|
246
|
+
* // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
|
|
247
|
+
* try {
|
|
248
|
+
* await container.resolve(ServiceA);
|
|
249
|
+
* } catch (error) {
|
|
250
|
+
* if (error instanceof DependencyCreationError) {
|
|
251
|
+
* console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
|
|
252
|
+
* const rootCause = error.getRootCause();
|
|
253
|
+
* console.error('Root cause:', rootCause); // Original error from ServiceC
|
|
254
|
+
* }
|
|
255
|
+
* }
|
|
256
|
+
* ```
|
|
243
257
|
*/
|
|
244
|
-
var DependencyCreationError = class extends
|
|
258
|
+
var DependencyCreationError = class DependencyCreationError extends SandlyError {
|
|
245
259
|
/**
|
|
246
260
|
* @internal
|
|
247
261
|
* Creates a DependencyCreationError wrapping the original factory error.
|
|
@@ -255,6 +269,31 @@ var DependencyCreationError = class extends ContainerError {
|
|
|
255
269
|
detail: { tag: Tag.id(tag) }
|
|
256
270
|
});
|
|
257
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Traverses the error chain to find the root cause error.
|
|
274
|
+
*
|
|
275
|
+
* When dependencies are nested, each level wraps the error in a DependencyCreationError.
|
|
276
|
+
* This method unwraps all the layers to get to the original error that started the failure.
|
|
277
|
+
*
|
|
278
|
+
* @returns The root cause error (not a DependencyCreationError unless that's the only error)
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```typescript
|
|
282
|
+
* try {
|
|
283
|
+
* await container.resolve(UserService);
|
|
284
|
+
* } catch (error) {
|
|
285
|
+
* if (error instanceof DependencyCreationError) {
|
|
286
|
+
* const rootCause = error.getRootCause();
|
|
287
|
+
* console.error('Root cause:', rootCause);
|
|
288
|
+
* }
|
|
289
|
+
* }
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
getRootCause() {
|
|
293
|
+
let current = this.cause;
|
|
294
|
+
while (current instanceof DependencyCreationError && current.cause !== void 0) current = current.cause;
|
|
295
|
+
return current;
|
|
296
|
+
}
|
|
258
297
|
};
|
|
259
298
|
/**
|
|
260
299
|
* Error thrown when one or more finalizers fail during container destruction.
|
|
@@ -275,7 +314,7 @@ var DependencyCreationError = class extends ContainerError {
|
|
|
275
314
|
* }
|
|
276
315
|
* ```
|
|
277
316
|
*/
|
|
278
|
-
var DependencyFinalizationError = class extends
|
|
317
|
+
var DependencyFinalizationError = class extends SandlyError {
|
|
279
318
|
/**
|
|
280
319
|
* @internal
|
|
281
320
|
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
@@ -283,22 +322,44 @@ var DependencyFinalizationError = class extends ContainerError {
|
|
|
283
322
|
* @param errors - Array of errors thrown by individual finalizers
|
|
284
323
|
*/
|
|
285
324
|
constructor(errors) {
|
|
286
|
-
const lambdaErrors = errors.map((error) =>
|
|
325
|
+
const lambdaErrors = errors.map((error) => SandlyError.ensure(error));
|
|
287
326
|
super("Error destroying dependency container", {
|
|
288
327
|
cause: errors[0],
|
|
289
328
|
detail: { errors: lambdaErrors.map((error) => error.dump()) }
|
|
290
329
|
});
|
|
330
|
+
this.errors = errors;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Returns the root causes of the errors that occurred during finalization.
|
|
334
|
+
*
|
|
335
|
+
* @returns An array of the errors that occurred during finalization.
|
|
336
|
+
* You can expect at least one error in the array.
|
|
337
|
+
*/
|
|
338
|
+
getRootCauses() {
|
|
339
|
+
return this.errors;
|
|
291
340
|
}
|
|
292
341
|
};
|
|
293
342
|
|
|
294
343
|
//#endregion
|
|
295
344
|
//#region src/container.ts
|
|
296
345
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
346
|
+
* Internal implementation of ResolutionContext that carries the resolution chain
|
|
347
|
+
* for circular dependency detection.
|
|
299
348
|
* @internal
|
|
300
349
|
*/
|
|
301
|
-
|
|
350
|
+
var ResolutionContextImpl = class {
|
|
351
|
+
constructor(resolveFn) {
|
|
352
|
+
this.resolveFn = resolveFn;
|
|
353
|
+
}
|
|
354
|
+
async resolve(tag) {
|
|
355
|
+
return this.resolveFn(tag);
|
|
356
|
+
}
|
|
357
|
+
async resolveAll(...tags) {
|
|
358
|
+
const promises = tags.map((tag) => this.resolve(tag));
|
|
359
|
+
const results = await Promise.all(promises);
|
|
360
|
+
return results;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
302
363
|
const ContainerTypeId = Symbol.for("sandly/Container");
|
|
303
364
|
/**
|
|
304
365
|
* A type-safe dependency injection container that manages service instantiation,
|
|
@@ -487,8 +548,9 @@ var Container = class Container {
|
|
|
487
548
|
this.factories.set(tag, spec);
|
|
488
549
|
this.finalizers.delete(tag);
|
|
489
550
|
} else {
|
|
490
|
-
this.factories.set(tag, spec.
|
|
491
|
-
this.finalizers.set(tag, spec.
|
|
551
|
+
this.factories.set(tag, spec.create.bind(spec));
|
|
552
|
+
if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
|
|
553
|
+
else this.finalizers.delete(tag);
|
|
492
554
|
}
|
|
493
555
|
return this;
|
|
494
556
|
}
|
|
@@ -526,8 +588,8 @@ var Container = class Container {
|
|
|
526
588
|
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
527
589
|
* requests for the same dependency correctly.
|
|
528
590
|
*
|
|
529
|
-
* The method performs circular dependency detection
|
|
530
|
-
* the resolution
|
|
591
|
+
* The method performs circular dependency detection by tracking the resolution chain
|
|
592
|
+
* through the resolution context.
|
|
531
593
|
*
|
|
532
594
|
* @template T - The dependency tag type (must be registered in this container)
|
|
533
595
|
* @param tag - The dependency tag to retrieve
|
|
@@ -570,21 +632,30 @@ var Container = class Container {
|
|
|
570
632
|
* ```
|
|
571
633
|
*/
|
|
572
634
|
async resolve(tag) {
|
|
635
|
+
return this.resolveInternal(tag, []);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Internal resolution method that tracks the dependency chain for circular dependency detection.
|
|
639
|
+
* Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
|
|
640
|
+
* @internal
|
|
641
|
+
*/
|
|
642
|
+
resolveInternal(tag, chain) {
|
|
573
643
|
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
|
|
574
644
|
const cached = this.cache.get(tag);
|
|
575
645
|
if (cached !== void 0) return cached;
|
|
576
|
-
|
|
577
|
-
if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
|
|
646
|
+
if (chain.includes(tag)) throw new CircularDependencyError(tag, chain);
|
|
578
647
|
const factory = this.factories.get(tag);
|
|
579
648
|
if (factory === void 0) throw new UnknownDependencyError(tag);
|
|
580
|
-
const
|
|
649
|
+
const newChain = [...chain, tag];
|
|
650
|
+
const context = new ResolutionContextImpl((tag$1) => this.resolveInternal(tag$1, newChain));
|
|
651
|
+
const instancePromise = (async () => {
|
|
581
652
|
try {
|
|
582
|
-
const instance = await factory(
|
|
653
|
+
const instance = await factory(context);
|
|
583
654
|
return instance;
|
|
584
655
|
} catch (error) {
|
|
585
656
|
throw new DependencyCreationError(tag, error);
|
|
586
657
|
}
|
|
587
|
-
}).catch((error) => {
|
|
658
|
+
})().catch((error) => {
|
|
588
659
|
this.cache.delete(tag);
|
|
589
660
|
throw error;
|
|
590
661
|
});
|
|
@@ -638,66 +709,15 @@ var Container = class Container {
|
|
|
638
709
|
return results;
|
|
639
710
|
}
|
|
640
711
|
/**
|
|
641
|
-
* Copies all registrations from this container to a target container.
|
|
642
|
-
*
|
|
643
|
-
* @internal
|
|
644
|
-
* @param target - The container to copy registrations to
|
|
645
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
646
|
-
*/
|
|
647
|
-
copyTo(target) {
|
|
648
|
-
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot copy registrations from a destroyed container");
|
|
649
|
-
for (const [tag, factory] of this.factories) {
|
|
650
|
-
const finalizer = this.finalizers.get(tag);
|
|
651
|
-
if (finalizer) target.register(tag, {
|
|
652
|
-
factory,
|
|
653
|
-
finalizer
|
|
654
|
-
});
|
|
655
|
-
else target.register(tag, factory);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Creates a new container by merging this container's registrations with another container.
|
|
660
|
-
*
|
|
661
|
-
* This method creates a new container that contains all registrations from both containers.
|
|
662
|
-
* If there are conflicts (same dependency registered in both containers), this
|
|
663
|
-
* container's registration will take precedence.
|
|
664
|
-
*
|
|
665
|
-
* **Important**: Only the registrations are copied, not any cached instances.
|
|
666
|
-
* The new container starts with an empty instance cache.
|
|
667
|
-
*
|
|
668
|
-
* @param other - The container to merge with
|
|
669
|
-
* @returns A new container with combined registrations
|
|
670
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
671
|
-
*
|
|
672
|
-
* @example Merging containers
|
|
673
|
-
* ```typescript
|
|
674
|
-
* const container1 = Container.empty()
|
|
675
|
-
* .register(DatabaseService, () => new DatabaseService());
|
|
676
|
-
*
|
|
677
|
-
* const container2 = Container.empty()
|
|
678
|
-
* .register(UserService, () => new UserService());
|
|
679
|
-
*
|
|
680
|
-
* const merged = container1.merge(container2);
|
|
681
|
-
* // merged has both DatabaseService and UserService
|
|
682
|
-
* ```
|
|
683
|
-
*/
|
|
684
|
-
merge(other) {
|
|
685
|
-
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
|
|
686
|
-
const merged = new Container();
|
|
687
|
-
other.copyTo(merged);
|
|
688
|
-
this.copyTo(merged);
|
|
689
|
-
return merged;
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
712
|
* Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
|
|
693
713
|
*
|
|
694
714
|
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
695
|
-
* Any subsequent calls to register(), get(), or destroy() will throw a
|
|
715
|
+
* Any subsequent calls to register(), get(), or destroy() will throw a DependencyFinalizationError.
|
|
696
716
|
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
697
717
|
*
|
|
698
718
|
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
699
719
|
* for maximum cleanup performance.
|
|
700
|
-
* If any finalizers fail, all errors are collected and a
|
|
720
|
+
* If any finalizers fail, all errors are collected and a DependencyFinalizationError
|
|
701
721
|
* is thrown containing details of all failures.
|
|
702
722
|
*
|
|
703
723
|
* **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
|
|
@@ -960,7 +980,14 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
960
980
|
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
961
981
|
*/
|
|
962
982
|
async resolve(tag) {
|
|
963
|
-
|
|
983
|
+
return this.resolveInternal(tag, []);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Internal resolution with delegation logic for scoped containers.
|
|
987
|
+
* @internal
|
|
988
|
+
*/
|
|
989
|
+
resolveInternal(tag, chain) {
|
|
990
|
+
if (this.factories.has(tag)) return super.resolveInternal(tag, chain);
|
|
964
991
|
if (this.parent !== null) return this.parent.resolve(tag);
|
|
965
992
|
throw new UnknownDependencyError(tag);
|
|
966
993
|
}
|
|
@@ -992,24 +1019,6 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
992
1019
|
if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
|
|
993
1020
|
}
|
|
994
1021
|
/**
|
|
995
|
-
* Creates a new scoped container by merging this container's registrations with another container.
|
|
996
|
-
*
|
|
997
|
-
* This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
|
|
998
|
-
* The resulting scoped container contains all registrations from both containers and becomes a root scope
|
|
999
|
-
* (no parent) with the scope name from this container.
|
|
1000
|
-
*
|
|
1001
|
-
* @param other - The container to merge with
|
|
1002
|
-
* @returns A new ScopedContainer with combined registrations
|
|
1003
|
-
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
1004
|
-
*/
|
|
1005
|
-
merge(other) {
|
|
1006
|
-
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
|
|
1007
|
-
const merged = new ScopedContainer(null, this.scope);
|
|
1008
|
-
other.copyTo(merged);
|
|
1009
|
-
this.copyTo(merged);
|
|
1010
|
-
return merged;
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
1022
|
* Creates a child scoped container.
|
|
1014
1023
|
*
|
|
1015
1024
|
* Child containers inherit access to parent dependencies but maintain
|
|
@@ -1022,52 +1031,6 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
1022
1031
|
return child;
|
|
1023
1032
|
}
|
|
1024
1033
|
};
|
|
1025
|
-
/**
|
|
1026
|
-
* Converts a regular container into a scoped container, copying all registrations.
|
|
1027
|
-
*
|
|
1028
|
-
* This function creates a new ScopedContainer instance and copies all factory functions
|
|
1029
|
-
* and finalizers from the source container. The resulting scoped container becomes a root
|
|
1030
|
-
* scope (no parent) with all the same dependency registrations.
|
|
1031
|
-
*
|
|
1032
|
-
* **Important**: Only the registrations are copied, not any cached instances.
|
|
1033
|
-
* The new scoped container starts with an empty instance cache.
|
|
1034
|
-
*
|
|
1035
|
-
* @param container - The container to convert to a scoped container
|
|
1036
|
-
* @param scope - A string or symbol identifier for this scope (used for debugging)
|
|
1037
|
-
* @returns A new ScopedContainer instance with all registrations copied from the source container
|
|
1038
|
-
* @throws {ContainerDestroyedError} If the source container has been destroyed
|
|
1039
|
-
*
|
|
1040
|
-
* @example Converting a regular container to scoped
|
|
1041
|
-
* ```typescript
|
|
1042
|
-
* import { container, scoped } from 'sandly';
|
|
1043
|
-
*
|
|
1044
|
-
* const appContainer = Container.empty()
|
|
1045
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
1046
|
-
* .register(ConfigService, () => new ConfigService());
|
|
1047
|
-
*
|
|
1048
|
-
* const scopedAppContainer = scoped(appContainer, 'app');
|
|
1049
|
-
*
|
|
1050
|
-
* // Create child scopes
|
|
1051
|
-
* const requestContainer = scopedAppContainer.child('request');
|
|
1052
|
-
* ```
|
|
1053
|
-
*
|
|
1054
|
-
* @example Copying complex registrations
|
|
1055
|
-
* ```typescript
|
|
1056
|
-
* const baseContainer = Container.empty()
|
|
1057
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
1058
|
-
* .register(UserService, {
|
|
1059
|
-
* factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
|
|
1060
|
-
* finalizer: (service) => service.cleanup()
|
|
1061
|
-
* });
|
|
1062
|
-
*
|
|
1063
|
-
* const scopedContainer = scoped(baseContainer, 'app');
|
|
1064
|
-
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
1065
|
-
* ```
|
|
1066
|
-
*/
|
|
1067
|
-
function scoped(container, scope) {
|
|
1068
|
-
const emptyScoped = ScopedContainer.empty(scope);
|
|
1069
|
-
return emptyScoped.merge(container);
|
|
1070
|
-
}
|
|
1071
1034
|
|
|
1072
1035
|
//#endregion
|
|
1073
1036
|
//#region src/service.ts
|
|
@@ -1212,14 +1175,14 @@ function service(tag, spec) {
|
|
|
1212
1175
|
* DatabaseService,
|
|
1213
1176
|
* {
|
|
1214
1177
|
* dependencies: ['postgresql://localhost:5432/mydb'],
|
|
1215
|
-
*
|
|
1178
|
+
* cleanup: (service) => service.disconnect() // Finalizer for cleanup
|
|
1216
1179
|
* }
|
|
1217
1180
|
* );
|
|
1218
1181
|
* ```
|
|
1219
1182
|
*/
|
|
1220
1183
|
function autoService(tag, spec) {
|
|
1221
1184
|
if (Array.isArray(spec)) spec = { dependencies: spec };
|
|
1222
|
-
const
|
|
1185
|
+
const create = async (ctx) => {
|
|
1223
1186
|
const diDeps = [];
|
|
1224
1187
|
for (const dep of spec.dependencies) if (Tag.isTag(dep)) diDeps.push(dep);
|
|
1225
1188
|
const resolved = await ctx.resolveAll(...diDeps);
|
|
@@ -1229,11 +1192,10 @@ function autoService(tag, spec) {
|
|
|
1229
1192
|
else args.push(dep);
|
|
1230
1193
|
return new tag(...args);
|
|
1231
1194
|
};
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1236
|
-
return service(tag, finalSpec);
|
|
1195
|
+
return service(tag, {
|
|
1196
|
+
create,
|
|
1197
|
+
cleanup: spec.cleanup
|
|
1198
|
+
});
|
|
1237
1199
|
}
|
|
1238
1200
|
|
|
1239
1201
|
//#endregion
|
|
@@ -1261,4 +1223,4 @@ function value(tag, constantValue) {
|
|
|
1261
1223
|
}
|
|
1262
1224
|
|
|
1263
1225
|
//#endregion
|
|
1264
|
-
export { CircularDependencyError, Container, ContainerDestroyedError,
|
|
1226
|
+
export { CircularDependencyError, Container, ContainerDestroyedError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, InjectSource, Layer, SandlyError, ScopedContainer, Tag, UnknownDependencyError, autoService, layer, service, value };
|
package/package.json
CHANGED
|
@@ -1,75 +1,76 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
2
|
+
"name": "sandly",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"typescript",
|
|
6
|
+
"sandly",
|
|
7
|
+
"dependency-injection",
|
|
8
|
+
"dependency-inversion",
|
|
9
|
+
"injection",
|
|
10
|
+
"di",
|
|
11
|
+
"inversion-of-control",
|
|
12
|
+
"ioc",
|
|
13
|
+
"container",
|
|
14
|
+
"layer",
|
|
15
|
+
"service",
|
|
16
|
+
"aws",
|
|
17
|
+
"lambda",
|
|
18
|
+
"aws-lambda"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/borisrakovan/sandly",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/borisrakovan/sandly/issues"
|
|
23
|
+
},
|
|
24
|
+
"author": "Boris Rakovan <b.rakovan@gmail.com> (https://github.com/borisrakovan)",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/borisrakovan/sandly.git"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"main": "dist/index.js",
|
|
35
|
+
"types": "dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"format": "prettier --write \"{src,test}/**/*.ts\"",
|
|
47
|
+
"format:check": "prettier --check \"{src,test}/**/*.ts\"",
|
|
48
|
+
"lint": "eslint . --fix",
|
|
49
|
+
"lint:check": "eslint . --max-warnings=0",
|
|
50
|
+
"type:check": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"release": "changeset version && changeset publish"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@changesets/cli": "^2.29.5",
|
|
56
|
+
"@eslint/js": "^9.26.0",
|
|
57
|
+
"@types/aws-lambda": "^8.10.149",
|
|
58
|
+
"@types/node": "^22.15.3",
|
|
59
|
+
"@types/node-fetch": "^2.6.12",
|
|
60
|
+
"dotenv": "^16.5.0",
|
|
61
|
+
"esbuild": "^0.25.3",
|
|
62
|
+
"eslint": "^9.26.0",
|
|
63
|
+
"node-fetch": "^3.3.2",
|
|
64
|
+
"prettier": "^3.5.3",
|
|
65
|
+
"prettier-plugin-organize-imports": "^4.1.0",
|
|
66
|
+
"ts-node": "^10.9.2",
|
|
67
|
+
"tsdown": "^0.14.1",
|
|
68
|
+
"tsx": "^4.19.4",
|
|
69
|
+
"typescript": "^5.8.3",
|
|
70
|
+
"typescript-eslint": "^8.38.0",
|
|
71
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
72
|
+
"vitest": "^3.1.3",
|
|
73
|
+
"zod": "^4.0.15"
|
|
74
|
+
},
|
|
75
|
+
"packageManager": "pnpm@9.13.0+sha512.beb9e2a803db336c10c9af682b58ad7181ca0fbd0d4119f2b33d5f2582e96d6c0d93c85b23869295b765170fbdaa92890c0da6ada457415039769edf3c959efe"
|
|
76
|
+
}
|