sandly 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -2609
- package/dist/index.d.ts +654 -1554
- package/dist/index.js +522 -921
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,1510 +1,894 @@
|
|
|
1
1
|
//#region src/tag.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Type representing a tag identifier (string or symbol).
|
|
4
|
-
* @internal
|
|
5
4
|
*/
|
|
6
5
|
type TagId = string | symbol;
|
|
7
6
|
/**
|
|
8
|
-
* Symbol used to identify
|
|
9
|
-
* This symbol is used as a property key to attach metadata to both value tags and service tags.
|
|
10
|
-
*
|
|
11
|
-
* Note: We can't use a symbol here becuase it produced the following TS error:
|
|
12
|
-
* error TS4020: 'extends' clause of exported class 'NotificationService' has or is using private name 'TagIdKey'.
|
|
13
|
-
*
|
|
7
|
+
* Symbol used to identify ValueTag objects at runtime.
|
|
14
8
|
* @internal
|
|
15
9
|
*/
|
|
16
10
|
declare const ValueTagIdKey = "sandly/ValueTagIdKey";
|
|
17
|
-
declare const ServiceTagIdKey = "sandly/ServiceTagIdKey";
|
|
18
11
|
/**
|
|
19
|
-
*
|
|
20
|
-
* This string is used as a property key to attach metadata to both value tags and service tags.
|
|
21
|
-
* It is used to carry the type of the tagged type and should not be used directly.
|
|
12
|
+
* Symbol used to carry the phantom type for ValueTag.
|
|
22
13
|
* @internal
|
|
23
14
|
*/
|
|
24
15
|
declare const TagTypeKey = "sandly/TagTypeKey";
|
|
25
16
|
/**
|
|
26
|
-
*
|
|
17
|
+
* A ServiceTag is any class constructor.
|
|
27
18
|
*
|
|
28
|
-
*
|
|
29
|
-
* strings, numbers, or any other values. They use phantom types to maintain type safety
|
|
30
|
-
* while being distinguishable at runtime through their unique identifiers.
|
|
19
|
+
* Any class can be used directly as a dependency tag without special markers.
|
|
31
20
|
*
|
|
32
|
-
* @template T - The type of
|
|
33
|
-
* @template Id - The unique identifier for this tag (string or symbol)
|
|
21
|
+
* @template T - The type of instances created by this class
|
|
34
22
|
*
|
|
35
23
|
* @example
|
|
36
24
|
* ```typescript
|
|
37
|
-
*
|
|
38
|
-
*
|
|
25
|
+
* class UserService {
|
|
26
|
+
* constructor(private db: Database) {}
|
|
27
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
28
|
+
* }
|
|
39
29
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
30
|
+
* const container = Container.builder()
|
|
31
|
+
* .add(UserService, ...)
|
|
32
|
+
* .build();
|
|
42
33
|
* ```
|
|
43
34
|
*/
|
|
44
|
-
|
|
45
|
-
readonly [ValueTagIdKey]: Id;
|
|
46
|
-
readonly [TagTypeKey]: T;
|
|
47
|
-
}
|
|
35
|
+
type ServiceTag<T = unknown> = new (...args: any[]) => T;
|
|
48
36
|
/**
|
|
49
|
-
*
|
|
37
|
+
* A ValueTag represents a non-class dependency (primitives, objects, functions).
|
|
50
38
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* that the DI system uses for identification and type safety.
|
|
39
|
+
* ValueTags use phantom types to maintain type safety while being
|
|
40
|
+
* distinguishable at runtime through their unique identifiers.
|
|
54
41
|
*
|
|
55
42
|
* @template Id - The unique identifier for this tag (string or symbol)
|
|
56
|
-
* @template T - The type of
|
|
43
|
+
* @template T - The type of the value this tag represents
|
|
57
44
|
*
|
|
58
45
|
* @example
|
|
59
46
|
* ```typescript
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* getUsers() { return []; }
|
|
63
|
-
* }
|
|
47
|
+
* const ApiKeyTag = Tag.of('ApiKey')<string>();
|
|
48
|
+
* const ConfigTag = Tag.of('Config')<{ dbUrl: string }>();
|
|
64
49
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
50
|
+
* const container = Container.builder()
|
|
51
|
+
* .add(ApiKeyTag, () => process.env.API_KEY!)
|
|
52
|
+
* .add(ConfigTag, () => ({ dbUrl: 'postgres://...' }))
|
|
53
|
+
* .build();
|
|
67
54
|
* ```
|
|
68
|
-
*
|
|
69
|
-
* @internal - Users should use Tag.Service() instead of working with this type directly
|
|
70
55
|
*/
|
|
71
|
-
interface
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
readonly [ServiceTagIdKey]?: Id;
|
|
56
|
+
interface ValueTag<Id extends TagId, T> {
|
|
57
|
+
readonly [ValueTagIdKey]: Id;
|
|
58
|
+
readonly [TagTypeKey]: T;
|
|
76
59
|
}
|
|
77
60
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* This type is essential for type inference throughout the DI system, allowing
|
|
81
|
-
* the container and layers to automatically determine what type of service
|
|
82
|
-
* a given tag represents without manual type annotations.
|
|
83
|
-
*
|
|
84
|
-
* @template T - Any dependency tag (ValueTag or ServiceTag)
|
|
85
|
-
* @returns The service type that the tag represents
|
|
86
|
-
*
|
|
87
|
-
* @example With value tags
|
|
88
|
-
* ```typescript
|
|
89
|
-
* const StringTag = Tag.of('myString')<string>();
|
|
90
|
-
* const ConfigTag = Tag.of('config')<{ apiKey: string }>();
|
|
91
|
-
*
|
|
92
|
-
* type StringService = TagType<typeof StringTag>; // string
|
|
93
|
-
* type ConfigService = TagType<typeof ConfigTag>; // { apiKey: string }
|
|
94
|
-
* ```
|
|
95
|
-
*
|
|
96
|
-
* @example With service tags
|
|
97
|
-
* ```typescript
|
|
98
|
-
* class UserService extends Tag.Service('UserService') {
|
|
99
|
-
* getUsers() { return []; }
|
|
100
|
-
* }
|
|
101
|
-
*
|
|
102
|
-
* type UserServiceType = TagType<typeof UserService>; // UserService
|
|
103
|
-
* ```
|
|
104
|
-
*
|
|
105
|
-
* @example Used in container methods
|
|
106
|
-
* ```typescript
|
|
107
|
-
* // The container uses TagType internally for type inference
|
|
108
|
-
* container.register(StringTag, () => 'hello'); // Factory must return string
|
|
109
|
-
* container.register(UserService, () => new UserService()); // Factory must return UserService
|
|
61
|
+
* Union type representing any valid dependency tag in the system.
|
|
110
62
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
63
|
+
* A tag can be either:
|
|
64
|
+
* - A class constructor (ServiceTag) - for class-based dependencies
|
|
65
|
+
* - A ValueTag - for non-class dependencies (primitives, objects, functions)
|
|
114
66
|
*/
|
|
115
|
-
type
|
|
67
|
+
type AnyTag = ServiceTag | ValueTag<TagId, any>;
|
|
116
68
|
/**
|
|
117
|
-
*
|
|
69
|
+
* Extracts the instance/value type from any dependency tag.
|
|
118
70
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* what can be used as a dependency identifier.
|
|
71
|
+
* - For ServiceTag (class): extracts the instance type
|
|
72
|
+
* - For ValueTag: extracts the value type
|
|
122
73
|
*
|
|
123
|
-
* @example
|
|
74
|
+
* @example
|
|
124
75
|
* ```typescript
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* ```
|
|
76
|
+
* class UserService { ... }
|
|
77
|
+
* const ConfigTag = Tag.of('Config')<{ url: string }>();
|
|
128
78
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
132
|
-
* // DatabaseService satisfies AnyTag
|
|
79
|
+
* type A = TagType<typeof UserService>; // UserService
|
|
80
|
+
* type B = TagType<typeof ConfigTag>; // { url: string }
|
|
133
81
|
* ```
|
|
134
82
|
*/
|
|
135
|
-
type AnyTag = ValueTag<
|
|
83
|
+
type TagType<T extends AnyTag> = T extends (new (...args: any[]) => infer Instance) ? Instance : T extends ValueTag<any, infer Value> ? Value : never;
|
|
136
84
|
/**
|
|
137
|
-
* Utility object
|
|
138
|
-
*
|
|
139
|
-
* The Tag object provides the primary API for creating both value tags and service tags
|
|
140
|
-
* used throughout the dependency injection system. It's the main entry point for
|
|
141
|
-
* defining dependencies in a type-safe way.
|
|
85
|
+
* Utility object for creating and working with tags.
|
|
142
86
|
*/
|
|
143
87
|
declare const Tag: {
|
|
144
88
|
/**
|
|
145
|
-
* Creates a
|
|
89
|
+
* Creates a ValueTag factory for non-class dependencies.
|
|
146
90
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
91
|
+
* @param id - The unique identifier for this tag (string or symbol)
|
|
92
|
+
* @returns A factory function that creates a ValueTag for the specified type
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const ApiKeyTag = Tag.of('ApiKey')<string>();
|
|
97
|
+
* const PortTag = Tag.of('Port')<number>();
|
|
98
|
+
* const ConfigTag = Tag.of('Config')<{ dbUrl: string; port: number }>();
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
|
|
102
|
+
/**
|
|
103
|
+
* Gets a string identifier for any tag, used for error messages.
|
|
150
104
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* @returns A factory function that creates value tags for the specified type
|
|
105
|
+
* For classes: uses static `Tag` property if present, otherwise `constructor.name`
|
|
106
|
+
* For ValueTags: uses the tag's id
|
|
154
107
|
*
|
|
155
|
-
* @example
|
|
108
|
+
* @example
|
|
156
109
|
* ```typescript
|
|
157
|
-
*
|
|
158
|
-
*
|
|
110
|
+
* class UserService {}
|
|
111
|
+
* Tag.id(UserService); // "UserService"
|
|
112
|
+
*
|
|
113
|
+
* class ApiClient {
|
|
114
|
+
* static readonly Tag = 'MyApiClient'; // Custom name
|
|
115
|
+
* }
|
|
116
|
+
* Tag.id(ApiClient); // "MyApiClient"
|
|
159
117
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost', port: 5432 }));
|
|
118
|
+
* const ConfigTag = Tag.of('app.config')<Config>();
|
|
119
|
+
* Tag.id(ConfigTag); // "app.config"
|
|
163
120
|
* ```
|
|
121
|
+
*/
|
|
122
|
+
id: (tag: AnyTag) => string;
|
|
123
|
+
/**
|
|
124
|
+
* Type guard to check if a value is a ServiceTag (class constructor).
|
|
164
125
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* const DB_CONFIG_SYM = Symbol('database-config');
|
|
168
|
-
* const ConfigTag = Tag.of(DB_CONFIG_SYM)<DatabaseConfig>();
|
|
126
|
+
* Returns true for class declarations, class expressions, and regular functions
|
|
127
|
+
* (which can be used as constructors in JavaScript).
|
|
169
128
|
*
|
|
170
|
-
*
|
|
129
|
+
* Returns false for arrow functions since they cannot be used with `new`.
|
|
130
|
+
*/
|
|
131
|
+
isServiceTag: (x: unknown) => x is ServiceTag;
|
|
132
|
+
/**
|
|
133
|
+
* Type guard to check if a value is a ValueTag.
|
|
134
|
+
*/
|
|
135
|
+
isValueTag: (x: unknown) => x is ValueTag<TagId, any>;
|
|
136
|
+
/**
|
|
137
|
+
* Type guard to check if a value is any kind of tag (ServiceTag or ValueTag).
|
|
138
|
+
*/
|
|
139
|
+
isTag: (x: unknown) => x is AnyTag;
|
|
140
|
+
}; //#endregion
|
|
141
|
+
//#region src/types.d.ts
|
|
142
|
+
/**
|
|
143
|
+
* Type representing a value that can be either synchronous or a Promise.
|
|
144
|
+
* Used throughout the DI system to support both sync and async factories/finalizers.
|
|
145
|
+
*/
|
|
146
|
+
type PromiseOrValue<T> = T | Promise<T>;
|
|
147
|
+
/**
|
|
148
|
+
* Variance marker types for type-level programming.
|
|
149
|
+
* Used to control how generic types behave with respect to subtyping.
|
|
150
|
+
*/
|
|
151
|
+
type Contravariant<A> = (_: A) => void;
|
|
152
|
+
type Covariant<A> = (_: never) => A;
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/layer.d.ts
|
|
156
|
+
/**
|
|
157
|
+
* Replaces the TTags type parameter in a builder type with a new type.
|
|
158
|
+
* Preserves the concrete builder type (ContainerBuilder or ScopedContainerBuilder).
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
type WithBuilderTags<TBuilder, TNewTags extends AnyTag> = TBuilder extends ScopedContainerBuilder ? ScopedContainerBuilder<TNewTags> : TBuilder extends ContainerBuilder ? ContainerBuilder<TNewTags> : IContainerBuilder<TNewTags>;
|
|
162
|
+
/**
|
|
163
|
+
* Defines what constitutes a valid dependency for a given parameter type T.
|
|
164
|
+
* A valid dependency is either:
|
|
165
|
+
* - A ServiceTag (class) whose instances are assignable to T
|
|
166
|
+
* - A ValueTag whose value type is assignable to T
|
|
167
|
+
* - A raw value of type T
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
type ValidDepFor<T> = (T extends object ? ServiceTag<T> | ValueTag<any, T> : ValueTag<any, T>) | T;
|
|
171
|
+
/**
|
|
172
|
+
* Maps constructor parameters to valid dependency types.
|
|
173
|
+
* Each parameter type T becomes ValidDepFor<T>.
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
type ValidDepsFor<TParams extends readonly unknown[]> = { readonly [K in keyof TParams]: ValidDepFor<TParams[K]> };
|
|
177
|
+
/**
|
|
178
|
+
* Extracts only the tags from a dependency array (filters out raw values).
|
|
179
|
+
* Used to determine layer requirements.
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
type ExtractTags<T extends readonly unknown[]> = { [K in keyof T]: T[K] extends AnyTag ? T[K] : never }[number];
|
|
183
|
+
/**
|
|
184
|
+
* The most generic layer type that accepts any concrete layer.
|
|
185
|
+
*/
|
|
186
|
+
type AnyLayer = Layer<any, any>;
|
|
187
|
+
/**
|
|
188
|
+
* The type ID for the Layer interface.
|
|
189
|
+
*/
|
|
190
|
+
declare const LayerTypeId: unique symbol;
|
|
191
|
+
/**
|
|
192
|
+
* Helper type that extracts the union of all requirements from an array of layers.
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
|
|
196
|
+
/**
|
|
197
|
+
* Helper type that extracts the union of all provisions from an array of layers.
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
|
|
201
|
+
/**
|
|
202
|
+
* A dependency layer represents a reusable, composable unit of dependency registrations.
|
|
203
|
+
* Layers allow you to organize your dependency injection setup into logical groups
|
|
204
|
+
* that can be combined and reused across different contexts.
|
|
205
|
+
*
|
|
206
|
+
* ## Type Variance
|
|
207
|
+
*
|
|
208
|
+
* - **TRequires (covariant)**: A layer requiring fewer dependencies can substitute one requiring more
|
|
209
|
+
* - **TProvides (contravariant)**: A layer providing more services can substitute one providing fewer
|
|
210
|
+
*
|
|
211
|
+
* @template TRequires - The union of tags this layer requires
|
|
212
|
+
* @template TProvides - The union of tags this layer provides
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* // Create layers using Layer.service(), Layer.value(), or Layer.create()
|
|
217
|
+
* const dbLayer = Layer.service(Database, []);
|
|
218
|
+
* const userLayer = Layer.service(UserService, [Database]);
|
|
219
|
+
*
|
|
220
|
+
* // Compose layers
|
|
221
|
+
* const appLayer = userLayer.provide(dbLayer);
|
|
222
|
+
*
|
|
223
|
+
* // Create container from layer
|
|
224
|
+
* const container = Container.from(appLayer);
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
|
|
228
|
+
readonly [LayerTypeId]?: {
|
|
229
|
+
readonly _TRequires: Covariant<TRequires>;
|
|
230
|
+
readonly _TProvides: Contravariant<TProvides>;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Applies this layer's registrations to a container builder.
|
|
234
|
+
* Works with both ContainerBuilder and ScopedContainerBuilder.
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
apply: <TBuilder extends IContainerBuilder<TRequires>>(builder: TBuilder) => WithBuilderTags<TBuilder, TRequires | TProvides>;
|
|
238
|
+
/**
|
|
239
|
+
* Provides a dependency layer to this layer, creating a pipeline.
|
|
240
|
+
* The result only exposes this layer's provisions (not the dependency's).
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const appLayer = apiLayer
|
|
245
|
+
* .provide(serviceLayer)
|
|
246
|
+
* .provide(databaseLayer);
|
|
171
247
|
* ```
|
|
248
|
+
*/
|
|
249
|
+
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
|
|
250
|
+
/**
|
|
251
|
+
* Provides a dependency layer and merges both layers' provisions.
|
|
252
|
+
* Unlike `.provide()`, this exposes both this layer's and the dependency's provisions.
|
|
172
253
|
*
|
|
173
|
-
* @example
|
|
254
|
+
* @example
|
|
174
255
|
* ```typescript
|
|
175
|
-
* const
|
|
176
|
-
*
|
|
256
|
+
* const infraLayer = dbLayer.provideMerge(configLayer);
|
|
257
|
+
* // Provides both Database and Config
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
|
|
261
|
+
/**
|
|
262
|
+
* Merges this layer with another independent layer.
|
|
263
|
+
* Combines their requirements and provisions.
|
|
177
264
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
181
268
|
* ```
|
|
269
|
+
*/
|
|
270
|
+
merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Consolidated Layer API for creating and composing dependency layers.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* // Define services
|
|
278
|
+
* class Database {
|
|
279
|
+
* query(sql: string) { return []; }
|
|
280
|
+
* }
|
|
281
|
+
*
|
|
282
|
+
* class UserService {
|
|
283
|
+
* constructor(private db: Database) {}
|
|
284
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
285
|
+
* }
|
|
286
|
+
*
|
|
287
|
+
* // Create layers
|
|
288
|
+
* const dbLayer = Layer.service(Database, []);
|
|
289
|
+
* const userLayer = Layer.service(UserService, [Database]);
|
|
290
|
+
*
|
|
291
|
+
* // Compose and create container
|
|
292
|
+
* const appLayer = userLayer.provide(dbLayer);
|
|
293
|
+
* const container = Container.from(appLayer);
|
|
294
|
+
*
|
|
295
|
+
* const users = await container.resolve(UserService);
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
declare const Layer: {
|
|
299
|
+
/**
|
|
300
|
+
* Creates a layer that provides a class service with automatic dependency injection.
|
|
301
|
+
*
|
|
302
|
+
* The dependencies array must match the constructor parameters exactly (order and types).
|
|
303
|
+
* This is validated at compile time.
|
|
304
|
+
*
|
|
305
|
+
* @param cls - The service class
|
|
306
|
+
* @param deps - Array of dependencies (tags or raw values) matching constructor params
|
|
307
|
+
* @param options - Optional cleanup function for the service
|
|
182
308
|
*
|
|
183
|
-
* @example
|
|
309
|
+
* @example
|
|
184
310
|
* ```typescript
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* port: number;
|
|
188
|
-
* database: string;
|
|
311
|
+
* class UserService {
|
|
312
|
+
* constructor(private db: Database, private apiKey: string) {}
|
|
189
313
|
* }
|
|
190
314
|
*
|
|
191
|
-
* const
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
315
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
316
|
+
*
|
|
317
|
+
* // Dependencies must match constructor: (Database, string)
|
|
318
|
+
* const userLayer = Layer.service(UserService, [Database, ApiKeyTag]);
|
|
319
|
+
*
|
|
320
|
+
* // Also works with raw values
|
|
321
|
+
* const userLayer2 = Layer.service(UserService, [Database, 'my-api-key']);
|
|
197
322
|
* ```
|
|
198
323
|
*/
|
|
199
|
-
|
|
324
|
+
service<TClass extends ServiceTag, const TDeps extends readonly unknown[]>(cls: TClass, deps: TDeps & ValidDepsFor<ConstructorParameters<TClass>>, options?: {
|
|
325
|
+
cleanup?: Finalizer<InstanceType<TClass>>;
|
|
326
|
+
}): Layer<ExtractTags<TDeps>, TClass>;
|
|
200
327
|
/**
|
|
201
|
-
* Creates a
|
|
328
|
+
* Creates a layer that provides a constant value or pre-instantiated instance.
|
|
202
329
|
*
|
|
203
|
-
*
|
|
204
|
-
* Classes that extend the returned base class become both the dependency identifier
|
|
205
|
-
* and the implementation, providing type safety and clear semantics.
|
|
330
|
+
* Works with both ValueTags (for constants) and ServiceTags (for pre-instantiated instances, useful in tests).
|
|
206
331
|
*
|
|
207
|
-
* @
|
|
208
|
-
* @param
|
|
209
|
-
* @returns A base class that can be extended to create tagged service classes
|
|
332
|
+
* @param tag - The tag (ValueTag or ServiceTag) to register
|
|
333
|
+
* @param value - The value or instance to provide
|
|
210
334
|
*
|
|
211
|
-
* @example
|
|
335
|
+
* @example ValueTag (constant)
|
|
212
336
|
* ```typescript
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* return ['alice', 'bob'];
|
|
216
|
-
* }
|
|
217
|
-
* }
|
|
337
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
338
|
+
* const ConfigTag = Tag.of('config')<{ port: number }>();
|
|
218
339
|
*
|
|
219
|
-
*
|
|
340
|
+
* const configLayer = Layer.value(ApiKeyTag, 'secret-key')
|
|
341
|
+
* .merge(Layer.value(ConfigTag, { port: 3000 }));
|
|
220
342
|
* ```
|
|
221
343
|
*
|
|
222
|
-
* @example
|
|
344
|
+
* @example ServiceTag (pre-instantiated instance, useful for testing)
|
|
223
345
|
* ```typescript
|
|
224
|
-
* class
|
|
225
|
-
*
|
|
346
|
+
* class UserService {
|
|
347
|
+
* getUsers() { return []; }
|
|
226
348
|
* }
|
|
227
349
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
350
|
+
* const mockUserService = new UserService();
|
|
351
|
+
* const testLayer = Layer.value(UserService, mockUserService);
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
value<T extends AnyTag>(tag: T, value: TagType<T>): Layer<never, T>;
|
|
355
|
+
/**
|
|
356
|
+
* Creates a custom layer with full control over the factory logic.
|
|
357
|
+
*
|
|
358
|
+
* Use this when you need custom instantiation logic that can't be expressed
|
|
359
|
+
* with `Layer.service()` or `Layer.value()`.
|
|
232
360
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* }
|
|
236
|
-
* }
|
|
361
|
+
* - `TRequires` is inferred from the `requires` array
|
|
362
|
+
* - `TProvides` is inferred from what `apply` adds to the builder
|
|
237
363
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* .register(UserRepository, async (ctx) =>
|
|
241
|
-
* new UserRepository(await ctx.resolve(DatabaseService))
|
|
242
|
-
* );
|
|
243
|
-
* ```
|
|
364
|
+
* @param options.requires - Array of tags this layer requires (use [] for no requirements)
|
|
365
|
+
* @param options.apply - Function that adds registrations to a builder
|
|
244
366
|
*
|
|
245
|
-
* @example
|
|
367
|
+
* @example
|
|
246
368
|
* ```typescript
|
|
247
|
-
*
|
|
369
|
+
* // Layer with dependencies - TProvides inferred from builder.add()
|
|
370
|
+
* const cacheLayer = Layer.create({
|
|
371
|
+
* requires: [Database],
|
|
372
|
+
* apply: (builder) => builder
|
|
373
|
+
* .add(Cache, async (ctx) => {
|
|
374
|
+
* const db = await ctx.resolve(Database);
|
|
375
|
+
* return new Cache(db, { ttl: 3600 });
|
|
376
|
+
* })
|
|
377
|
+
* });
|
|
378
|
+
* // Type: Layer<typeof Database, typeof Cache>
|
|
248
379
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
380
|
+
* // Layer with no dependencies
|
|
381
|
+
* const dbLayer = Layer.create({
|
|
382
|
+
* requires: [],
|
|
383
|
+
* apply: (builder) => builder.add(Database, () => new Database())
|
|
384
|
+
* });
|
|
385
|
+
* // Type: Layer<never, typeof Database>
|
|
252
386
|
* ```
|
|
253
387
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
388
|
+
create<const TRequires extends readonly AnyTag[], TAllTags extends AnyTag>(options: {
|
|
389
|
+
requires: TRequires;
|
|
390
|
+
apply: (builder: IContainerBuilder<TRequires[number]>) => IContainerBuilder<TAllTags>;
|
|
391
|
+
}): Layer<TRequires[number], Exclude<TAllTags, TRequires[number]>>;
|
|
257
392
|
/**
|
|
258
|
-
*
|
|
393
|
+
* Creates an empty layer with no requirements or provisions.
|
|
259
394
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```typescript
|
|
397
|
+
* const baseLayer = Layer.empty()
|
|
398
|
+
* .merge(configLayer)
|
|
399
|
+
* .merge(serviceLayer);
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
empty(): Layer<never, never>;
|
|
403
|
+
/**
|
|
404
|
+
* Merges multiple layers at once.
|
|
263
405
|
*
|
|
264
|
-
* @param
|
|
265
|
-
* @returns
|
|
406
|
+
* @param layers - At least 2 layers to merge
|
|
407
|
+
* @returns A layer combining all requirements and provisions
|
|
266
408
|
*
|
|
267
409
|
* @example
|
|
268
410
|
* ```typescript
|
|
269
|
-
* const
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
411
|
+
* const infraLayer = Layer.mergeAll(
|
|
412
|
+
* persistenceLayer,
|
|
413
|
+
* messagingLayer,
|
|
414
|
+
* observabilityLayer
|
|
415
|
+
* );
|
|
274
416
|
* ```
|
|
275
|
-
*
|
|
276
|
-
* @internal - Primarily for internal use in error messages and debugging
|
|
277
417
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
418
|
+
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
419
|
+
/**
|
|
420
|
+
* Merges exactly two layers.
|
|
421
|
+
* Equivalent to `layer1.merge(layer2)`.
|
|
422
|
+
*/
|
|
423
|
+
merge<TRequires1 extends AnyTag, TProvides1 extends AnyTag, TRequires2 extends AnyTag, TProvides2 extends AnyTag>(layer1: Layer<TRequires1, TProvides1>, layer2: Layer<TRequires2, TProvides2>): Layer<TRequires1 | TRequires2, TProvides1 | TProvides2>;
|
|
280
424
|
};
|
|
281
|
-
/**
|
|
282
|
-
* String used to store the original ValueTag in Inject<T> types.
|
|
283
|
-
* This prevents property name collisions while allowing type-level extraction.
|
|
284
|
-
*/
|
|
285
|
-
declare const InjectSource = "sandly/InjectSource";
|
|
286
|
-
/**
|
|
287
|
-
* Helper type for injecting ValueTag dependencies in constructor parameters.
|
|
288
|
-
* This allows clean specification of ValueTag dependencies while preserving
|
|
289
|
-
* the original tag information for dependency inference.
|
|
290
|
-
*
|
|
291
|
-
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
292
|
-
*
|
|
293
|
-
* @template T - A ValueTag type
|
|
294
|
-
* @returns The value type with optional phantom tag metadata for dependency inference
|
|
295
|
-
*
|
|
296
|
-
* @example
|
|
297
|
-
* ```typescript
|
|
298
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
299
|
-
*
|
|
300
|
-
* class UserService extends Tag.Service('UserService') {
|
|
301
|
-
* constructor(
|
|
302
|
-
* private db: DatabaseService, // ServiceTag - works automatically
|
|
303
|
-
* private apiKey: Inject<typeof ApiKeyTag> // ValueTag - type is string, tag preserved
|
|
304
|
-
* ) {
|
|
305
|
-
* super();
|
|
306
|
-
* }
|
|
307
|
-
* }
|
|
308
|
-
* ```
|
|
309
|
-
*/
|
|
310
|
-
type Inject<T extends ValueTag<TagId, unknown>> = T extends ValueTag<any, infer V> ? V & {
|
|
311
|
-
readonly [InjectSource]?: T;
|
|
312
|
-
} : never;
|
|
313
|
-
/**
|
|
314
|
-
* Helper type to extract the original ValueTag from an Inject<T> type.
|
|
315
|
-
* Since InjectSource is optional, we need to check for both presence and absence.
|
|
316
|
-
* @internal
|
|
317
|
-
*/
|
|
318
|
-
type ExtractInjectTag<T> = T extends {
|
|
319
|
-
readonly [InjectSource]?: infer U;
|
|
320
|
-
} ? U : never; //#endregion
|
|
321
|
-
//#region src/types.d.ts
|
|
322
|
-
type PromiseOrValue<T> = T | Promise<T>;
|
|
323
|
-
type Contravariant<A> = (_: A) => void;
|
|
324
|
-
type Covariant<A> = (_: never) => A;
|
|
325
425
|
|
|
326
426
|
//#endregion
|
|
327
427
|
//#region src/container.d.ts
|
|
328
428
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* Factory functions are the core mechanism for dependency creation in the DI system.
|
|
332
|
-
* They receive a dependency container and can use it to resolve other dependencies
|
|
333
|
-
* that the service being created needs.
|
|
429
|
+
* Factory function that creates a dependency instance.
|
|
334
430
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
431
|
+
* Receives a resolution context for injecting other dependencies.
|
|
432
|
+
* Can be synchronous or asynchronous.
|
|
337
433
|
*
|
|
338
434
|
* @template T - The type of the service instance being created
|
|
339
|
-
* @template TRequires - Union type of
|
|
340
|
-
*
|
|
341
|
-
* @example Synchronous factory
|
|
342
|
-
* ```typescript
|
|
343
|
-
* const factory: Factory<DatabaseService, never> = (ctx) => {
|
|
344
|
-
* return new DatabaseService('sqlite://memory');
|
|
345
|
-
* };
|
|
346
|
-
* ```
|
|
347
|
-
*
|
|
348
|
-
* @example Asynchronous factory with dependencies
|
|
349
|
-
* ```typescript
|
|
350
|
-
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (ctx) => {
|
|
351
|
-
* const [config, db] = await Promise.all([
|
|
352
|
-
* ctx.resolve(ConfigTag),
|
|
353
|
-
* ctx.resolve(DatabaseService)
|
|
354
|
-
* ]);
|
|
355
|
-
* return new UserService(config, db);
|
|
356
|
-
* };
|
|
357
|
-
* ```
|
|
435
|
+
* @template TRequires - Union type of required dependencies
|
|
358
436
|
*/
|
|
359
437
|
type Factory<T, TRequires extends AnyTag> = (ctx: ResolutionContext<TRequires>) => PromiseOrValue<T>;
|
|
360
438
|
/**
|
|
361
|
-
*
|
|
439
|
+
* Cleanup function called when the container is destroyed.
|
|
362
440
|
*
|
|
363
|
-
*
|
|
364
|
-
* is destroyed via `container.destroy()`. They receive the created instance
|
|
365
|
-
* and should perform any necessary cleanup (closing connections, releasing resources, etc.).
|
|
366
|
-
*
|
|
367
|
-
* Like factories, finalizers can be either synchronous or asynchronous.
|
|
368
|
-
* All finalizers are called concurrently during container destruction.
|
|
369
|
-
*
|
|
370
|
-
* @template T - The type of the service instance being finalized
|
|
371
|
-
*
|
|
372
|
-
* @example Synchronous finalizer
|
|
373
|
-
* ```typescript
|
|
374
|
-
* const cleanup: Finalizer<FileHandle> = (fileHandle) => {
|
|
375
|
-
* fileHandle.close();
|
|
376
|
-
* };
|
|
377
|
-
* ```
|
|
378
|
-
*
|
|
379
|
-
* @example Asynchronous finalizer
|
|
380
|
-
* ```typescript
|
|
381
|
-
* const cleanup: Finalizer<DatabaseConnection> = async (connection) => {
|
|
382
|
-
* await connection.disconnect();
|
|
383
|
-
* };
|
|
384
|
-
* ```
|
|
385
|
-
*
|
|
386
|
-
* @example Resilient finalizer
|
|
387
|
-
* ```typescript
|
|
388
|
-
* const cleanup: Finalizer<HttpServer> = async (server) => {
|
|
389
|
-
* try {
|
|
390
|
-
* await server.close();
|
|
391
|
-
* } catch (error) {
|
|
392
|
-
* if (!error.message.includes('already closed')) {
|
|
393
|
-
* throw error; // Re-throw unexpected errors
|
|
394
|
-
* }
|
|
395
|
-
* // Ignore "already closed" errors
|
|
396
|
-
* }
|
|
397
|
-
* };
|
|
398
|
-
* ```
|
|
441
|
+
* @template T - The type of the service instance being cleaned up
|
|
399
442
|
*/
|
|
400
443
|
type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
401
444
|
/**
|
|
402
|
-
*
|
|
445
|
+
* Complete dependency lifecycle with factory and optional cleanup.
|
|
403
446
|
*
|
|
404
|
-
*
|
|
405
|
-
* passing separate factory and finalizer parameters, you can pass an object
|
|
406
|
-
* containing both.
|
|
407
|
-
*
|
|
408
|
-
* Since this is an interface, you can also implement it as a class for better
|
|
409
|
-
* organization and reuse. This is particularly useful when you have complex
|
|
410
|
-
* lifecycle logic or want to share lifecycle definitions across multiple services.
|
|
447
|
+
* Can be implemented as a class for complex lifecycle logic.
|
|
411
448
|
*
|
|
412
449
|
* @template T - The instance type
|
|
413
|
-
* @template TRequires - Union type of
|
|
414
|
-
*
|
|
415
|
-
* @example Using DependencyLifecycle as an object
|
|
416
|
-
* ```typescript
|
|
417
|
-
* import { Container, Tag } from 'sandly';
|
|
418
|
-
*
|
|
419
|
-
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
420
|
-
* async connect() { return; }
|
|
421
|
-
* async disconnect() { return; }
|
|
422
|
-
* }
|
|
423
|
-
*
|
|
424
|
-
* const lifecycle: DependencyLifecycle<DatabaseConnection, never> = {
|
|
425
|
-
* create: async () => {
|
|
426
|
-
* const conn = new DatabaseConnection();
|
|
427
|
-
* await conn.connect();
|
|
428
|
-
* return conn;
|
|
429
|
-
* },
|
|
430
|
-
* cleanup: async (conn) => {
|
|
431
|
-
* await conn.disconnect();
|
|
432
|
-
* }
|
|
433
|
-
* };
|
|
434
|
-
*
|
|
435
|
-
* Container.empty().register(DatabaseConnection, lifecycle);
|
|
436
|
-
* ```
|
|
437
|
-
*
|
|
438
|
-
* @example Implementing DependencyLifecycle as a class with dependencies
|
|
439
|
-
* ```typescript
|
|
440
|
-
* import { Container, Tag, type ResolutionContext } from 'sandly';
|
|
441
|
-
*
|
|
442
|
-
* class Logger extends Tag.Service('Logger') {
|
|
443
|
-
* log(message: string) { console.log(message); }
|
|
444
|
-
* }
|
|
445
|
-
*
|
|
446
|
-
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
447
|
-
* constructor(private logger: Logger, private url: string) { super(); }
|
|
448
|
-
* async connect() { this.logger.log('Connected'); }
|
|
449
|
-
* async disconnect() { this.logger.log('Disconnected'); }
|
|
450
|
-
* }
|
|
451
|
-
*
|
|
452
|
-
* class DatabaseLifecycle implements DependencyLifecycle<DatabaseConnection, typeof Logger> {
|
|
453
|
-
* constructor(private url: string) {}
|
|
454
|
-
*
|
|
455
|
-
* async create(ctx: ResolutionContext<typeof Logger>): Promise<DatabaseConnection> {
|
|
456
|
-
* const logger = await ctx.resolve(Logger);
|
|
457
|
-
* const conn = new DatabaseConnection(logger, this.url);
|
|
458
|
-
* await conn.connect();
|
|
459
|
-
* return conn;
|
|
460
|
-
* }
|
|
461
|
-
*
|
|
462
|
-
* async cleanup(conn: DatabaseConnection): Promise<void> {
|
|
463
|
-
* await conn.disconnect();
|
|
464
|
-
* }
|
|
465
|
-
* }
|
|
466
|
-
*
|
|
467
|
-
* const container = Container.empty()
|
|
468
|
-
* .register(Logger, () => new Logger())
|
|
469
|
-
* .register(DatabaseConnection, new DatabaseLifecycle('postgresql://localhost:5432'));
|
|
470
|
-
* ```
|
|
471
|
-
*
|
|
472
|
-
* @example Class with only factory (no cleanup)
|
|
473
|
-
* ```typescript
|
|
474
|
-
* import { Container, Tag } from 'sandly';
|
|
475
|
-
*
|
|
476
|
-
* class SimpleService extends Tag.Service('SimpleService') {}
|
|
477
|
-
*
|
|
478
|
-
* class SimpleServiceLifecycle implements DependencyLifecycle<SimpleService, never> {
|
|
479
|
-
* create(): SimpleService {
|
|
480
|
-
* return new SimpleService();
|
|
481
|
-
* }
|
|
482
|
-
* // cleanup is optional, so it can be omitted
|
|
483
|
-
* }
|
|
484
|
-
*
|
|
485
|
-
* const container = Container.empty().register(
|
|
486
|
-
* SimpleService,
|
|
487
|
-
* new SimpleServiceLifecycle()
|
|
488
|
-
* );
|
|
489
|
-
* ```
|
|
450
|
+
* @template TRequires - Union type of required dependencies
|
|
490
451
|
*/
|
|
491
452
|
interface DependencyLifecycle<T, TRequires extends AnyTag> {
|
|
492
453
|
create: Factory<T, TRequires>;
|
|
493
454
|
cleanup?: Finalizer<T>;
|
|
494
455
|
}
|
|
495
456
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* A dependency can be registered either as:
|
|
499
|
-
* - A simple factory function that creates the dependency
|
|
500
|
-
* - A complete lifecycle object with both factory and finalizer
|
|
501
|
-
*
|
|
502
|
-
* @template T - The dependency tag type
|
|
503
|
-
* @template TRequires - Union type of all required dependencies
|
|
504
|
-
*
|
|
505
|
-
* @example Simple factory registration
|
|
506
|
-
* ```typescript
|
|
507
|
-
* const spec: DependencySpec<typeof UserService, never> =
|
|
508
|
-
* () => new UserService();
|
|
509
|
-
*
|
|
510
|
-
* Container.empty().register(UserService, spec);
|
|
511
|
-
* ```
|
|
512
|
-
*
|
|
513
|
-
* @example Lifecycle registration
|
|
514
|
-
* ```typescript
|
|
515
|
-
* const spec: DependencySpec<typeof DatabaseConnection, never> = {
|
|
516
|
-
* create: () => new DatabaseConnection(),
|
|
517
|
-
* cleanup: (conn) => conn.close()
|
|
518
|
-
* };
|
|
519
|
-
*
|
|
520
|
-
* Container.empty().register(DatabaseConnection, spec);
|
|
521
|
-
* ```
|
|
457
|
+
* Valid dependency registration: either a factory function or lifecycle object.
|
|
522
458
|
*/
|
|
523
459
|
type DependencySpec<T extends AnyTag, TRequires extends AnyTag> = Factory<TagType<T>, TRequires> | DependencyLifecycle<TagType<T>, TRequires>;
|
|
524
460
|
/**
|
|
525
|
-
*
|
|
461
|
+
* Context available to factory functions during resolution.
|
|
526
462
|
*
|
|
527
|
-
*
|
|
528
|
-
* other dependencies during the creation of a service.
|
|
529
|
-
*
|
|
530
|
-
* @template TTags - Union type of all dependencies available in the container
|
|
463
|
+
* Provides `resolve` and `resolveAll` for injecting dependencies.
|
|
531
464
|
*/
|
|
532
465
|
type ResolutionContext<TTags extends AnyTag> = Pick<IContainer<TTags>, 'resolve' | 'resolveAll'>;
|
|
466
|
+
/**
|
|
467
|
+
* Unique symbol for container type branding.
|
|
468
|
+
*/
|
|
533
469
|
declare const ContainerTypeId: unique symbol;
|
|
534
470
|
/**
|
|
535
|
-
* Interface
|
|
471
|
+
* Interface for dependency containers.
|
|
536
472
|
*
|
|
537
|
-
* @template TTags - Union type of
|
|
473
|
+
* @template TTags - Union type of registered dependency tags
|
|
538
474
|
*/
|
|
539
475
|
interface IContainer<TTags extends AnyTag = never> {
|
|
540
476
|
readonly [ContainerTypeId]: {
|
|
541
477
|
readonly _TTags: Contravariant<TTags>;
|
|
542
478
|
};
|
|
543
|
-
register: <T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>) => IContainer<TTags | T>;
|
|
544
|
-
has(tag: AnyTag): boolean;
|
|
545
|
-
exists(tag: AnyTag): boolean;
|
|
546
479
|
resolve: <T extends TTags>(tag: T) => Promise<TagType<T>>;
|
|
547
480
|
resolveAll: <const T extends readonly TTags[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
481
|
+
use: <T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>) => Promise<R>;
|
|
548
482
|
destroy(): Promise<void>;
|
|
549
483
|
}
|
|
550
484
|
/**
|
|
551
|
-
* Extracts the registered tags
|
|
485
|
+
* Extracts the registered tags from a container type.
|
|
552
486
|
*/
|
|
553
487
|
type ContainerTags<C> = C extends IContainer<infer TTags> ? TTags : never;
|
|
554
488
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
|
|
489
|
+
* Common interface for container builders that support adding dependencies.
|
|
490
|
+
* Used by Layer to work with both ContainerBuilder and ScopedContainerBuilder.
|
|
491
|
+
*/
|
|
492
|
+
interface IContainerBuilder<TTags extends AnyTag = never> {
|
|
493
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): IContainerBuilder<TTags | T>;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Extracts the registered tags from a builder type.
|
|
497
|
+
*/
|
|
498
|
+
type BuilderTags<B> = B extends IContainerBuilder<infer TTags> ? TTags : never;
|
|
499
|
+
/**
|
|
500
|
+
* Builder for constructing immutable containers.
|
|
558
501
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* and preventing runtime errors.
|
|
502
|
+
* Use `Container.builder()` to create a builder, then chain `.add()` calls
|
|
503
|
+
* to register dependencies, and finally call `.build()` to create the container.
|
|
562
504
|
*
|
|
563
|
-
* @template TTags - Union type of
|
|
505
|
+
* @template TTags - Union type of registered dependency tags
|
|
564
506
|
*
|
|
565
|
-
* @example
|
|
507
|
+
* @example
|
|
566
508
|
* ```typescript
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
* class UserService extends Tag.Service('UserService') {
|
|
574
|
-
* constructor(private db: DatabaseService) {}
|
|
575
|
-
* getUser() { return this.db.query(); }
|
|
576
|
-
* }
|
|
577
|
-
*
|
|
578
|
-
* const container = Container.empty()
|
|
579
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
580
|
-
* .register(UserService, async (ctx) =>
|
|
581
|
-
* new UserService(await ctx.resolve(DatabaseService))
|
|
582
|
-
* );
|
|
583
|
-
*
|
|
584
|
-
* const userService = await c.resolve(UserService);
|
|
509
|
+
* const container = Container.builder()
|
|
510
|
+
* .add(Database, () => new Database())
|
|
511
|
+
* .add(UserService, async (ctx) =>
|
|
512
|
+
* new UserService(await ctx.resolve(Database))
|
|
513
|
+
* )
|
|
514
|
+
* .build();
|
|
585
515
|
* ```
|
|
516
|
+
*/
|
|
517
|
+
declare class ContainerBuilder<TTags extends AnyTag = never> {
|
|
518
|
+
private readonly factories;
|
|
519
|
+
private readonly finalizers;
|
|
520
|
+
/**
|
|
521
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
522
|
+
*
|
|
523
|
+
* @param tag - The dependency tag (class or ValueTag)
|
|
524
|
+
* @param spec - Factory function or lifecycle object
|
|
525
|
+
* @returns The builder with updated type information
|
|
526
|
+
*/
|
|
527
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ContainerBuilder<TTags | T>;
|
|
528
|
+
/**
|
|
529
|
+
* Creates an immutable container from the registered dependencies.
|
|
530
|
+
*/
|
|
531
|
+
build(): Container<TTags>;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Type-safe dependency injection container.
|
|
586
535
|
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
590
|
-
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
536
|
+
* Containers are immutable - use `Container.builder()` to create one.
|
|
537
|
+
* Each dependency is created once (singleton) and cached.
|
|
591
538
|
*
|
|
592
|
-
*
|
|
593
|
-
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
594
|
-
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
539
|
+
* @template TTags - Union type of registered dependency tags
|
|
595
540
|
*
|
|
596
|
-
*
|
|
597
|
-
* const config = await c.resolve(ConfigTag);
|
|
598
|
-
* ```
|
|
599
|
-
*
|
|
600
|
-
* @example With finalizers for cleanup
|
|
541
|
+
* @example
|
|
601
542
|
* ```typescript
|
|
602
|
-
* class
|
|
603
|
-
*
|
|
604
|
-
*
|
|
543
|
+
* class Database {
|
|
544
|
+
* query(sql: string) { return []; }
|
|
545
|
+
* }
|
|
546
|
+
*
|
|
547
|
+
* class UserService {
|
|
548
|
+
* constructor(private db: Database) {}
|
|
549
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
605
550
|
* }
|
|
606
551
|
*
|
|
607
|
-
* const container = Container.
|
|
608
|
-
*
|
|
609
|
-
* async () =>
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* },
|
|
614
|
-
* async (conn) => conn.disconnect() // Finalizer for cleanup
|
|
615
|
-
* );
|
|
552
|
+
* const container = Container.builder()
|
|
553
|
+
* .add(Database, () => new Database())
|
|
554
|
+
* .add(UserService, async (ctx) =>
|
|
555
|
+
* new UserService(await ctx.resolve(Database))
|
|
556
|
+
* )
|
|
557
|
+
* .build();
|
|
616
558
|
*
|
|
617
|
-
*
|
|
618
|
-
* await c.destroy(); // Calls all finalizers
|
|
559
|
+
* const userService = await container.resolve(UserService);
|
|
619
560
|
* ```
|
|
620
561
|
*/
|
|
621
|
-
declare class Container<TTags extends AnyTag> implements IContainer<TTags> {
|
|
562
|
+
declare class Container<TTags extends AnyTag = never> implements IContainer<TTags> {
|
|
622
563
|
readonly [ContainerTypeId]: {
|
|
623
564
|
readonly _TTags: Contravariant<TTags>;
|
|
624
565
|
};
|
|
625
|
-
protected constructor();
|
|
626
566
|
/**
|
|
627
|
-
* Cache of instantiated dependencies
|
|
628
|
-
* Ensures singleton behavior and supports concurrent access.
|
|
567
|
+
* Cache of instantiated dependencies.
|
|
629
568
|
* @internal
|
|
630
569
|
*/
|
|
631
570
|
protected readonly cache: Map<AnyTag, Promise<unknown>>;
|
|
632
571
|
/**
|
|
633
|
-
* Factory functions for creating
|
|
572
|
+
* Factory functions for creating dependencies.
|
|
634
573
|
* @internal
|
|
635
574
|
*/
|
|
636
575
|
protected readonly factories: Map<AnyTag, Factory<unknown, TTags>>;
|
|
637
576
|
/**
|
|
638
|
-
*
|
|
577
|
+
* Cleanup functions for dependencies.
|
|
639
578
|
* @internal
|
|
640
579
|
*/
|
|
641
580
|
protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
|
|
642
581
|
/**
|
|
643
|
-
*
|
|
582
|
+
* Whether this container has been destroyed.
|
|
644
583
|
* @internal
|
|
645
584
|
*/
|
|
646
585
|
protected isDestroyed: boolean;
|
|
647
586
|
/**
|
|
648
|
-
*
|
|
649
|
-
* @returns A new empty Container instance with no registered dependencies.
|
|
587
|
+
* @internal - Use Container.builder() or Container.empty()
|
|
650
588
|
*/
|
|
651
|
-
|
|
589
|
+
protected constructor(factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
|
|
652
590
|
/**
|
|
653
|
-
*
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* If a dependency is already registered, this method will override it unless the
|
|
660
|
-
* dependency has already been instantiated, in which case it will throw an error.
|
|
661
|
-
*
|
|
662
|
-
* @template T - The dependency tag being registered
|
|
663
|
-
* @param tag - The dependency tag (class or value tag)
|
|
664
|
-
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
665
|
-
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
666
|
-
* @returns A new container instance with the dependency registered
|
|
667
|
-
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
668
|
-
* @throws {Error} If the dependency has already been instantiated
|
|
669
|
-
*
|
|
670
|
-
* @example Registering a simple service
|
|
671
|
-
* ```typescript
|
|
672
|
-
* class LoggerService extends Tag.Service('LoggerService') {
|
|
673
|
-
* log(message: string) { console.log(message); }
|
|
674
|
-
* }
|
|
675
|
-
*
|
|
676
|
-
* const container = Container.empty().register(
|
|
677
|
-
* LoggerService,
|
|
678
|
-
* () => new LoggerService()
|
|
679
|
-
* );
|
|
680
|
-
* ```
|
|
591
|
+
* @internal - Used by ContainerBuilder
|
|
592
|
+
*/
|
|
593
|
+
static _createFromBuilder<T extends AnyTag>(factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): Container<T>;
|
|
594
|
+
/**
|
|
595
|
+
* Creates a new container builder.
|
|
681
596
|
*
|
|
682
|
-
* @example
|
|
597
|
+
* @example
|
|
683
598
|
* ```typescript
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
* const container = Container.empty()
|
|
689
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
690
|
-
* .register(LoggerService, () => new LoggerService())
|
|
691
|
-
* .register(UserService, async (ctx) =>
|
|
692
|
-
* new UserService(
|
|
693
|
-
* await ctx.resolve(DatabaseService),
|
|
694
|
-
* await ctx.resolve(LoggerService)
|
|
695
|
-
* )
|
|
696
|
-
* );
|
|
599
|
+
* const container = Container.builder()
|
|
600
|
+
* .add(Database, () => new Database())
|
|
601
|
+
* .build();
|
|
697
602
|
* ```
|
|
603
|
+
*/
|
|
604
|
+
static builder(): ContainerBuilder;
|
|
605
|
+
/**
|
|
606
|
+
* Creates an empty container with no dependencies.
|
|
698
607
|
*
|
|
699
|
-
*
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
*
|
|
704
|
-
* ```
|
|
608
|
+
* Shorthand for `Container.builder().build()`.
|
|
609
|
+
*/
|
|
610
|
+
static empty(): Container;
|
|
611
|
+
/**
|
|
612
|
+
* Creates a scoped container for hierarchical dependency management.
|
|
705
613
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
614
|
+
* Scoped containers support parent/child relationships where children
|
|
615
|
+
* can access parent dependencies but maintain their own cache.
|
|
709
616
|
*
|
|
710
|
-
*
|
|
711
|
-
* ConfigTag,
|
|
712
|
-
* () => ({ apiUrl: 'https://api.example.com' })
|
|
713
|
-
* );
|
|
714
|
-
* ```
|
|
617
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
715
618
|
*
|
|
716
|
-
* @example
|
|
619
|
+
* @example
|
|
717
620
|
* ```typescript
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* async close() { return; }
|
|
721
|
-
* }
|
|
621
|
+
* const appContainer = Container.scoped('app');
|
|
622
|
+
* // ... add app-level dependencies
|
|
722
623
|
*
|
|
723
|
-
* const
|
|
724
|
-
*
|
|
725
|
-
* async () => {
|
|
726
|
-
* const conn = new DatabaseConnection();
|
|
727
|
-
* await conn.connect();
|
|
728
|
-
* return conn;
|
|
729
|
-
* },
|
|
730
|
-
* (conn) => conn.close() // Called during container.destroy()
|
|
731
|
-
* );
|
|
624
|
+
* const requestContainer = appContainer.child('request');
|
|
625
|
+
* // ... add request-specific dependencies
|
|
732
626
|
* ```
|
|
733
627
|
*/
|
|
734
|
-
|
|
628
|
+
static scoped(scope: string | symbol): ScopedContainer;
|
|
735
629
|
/**
|
|
736
|
-
*
|
|
630
|
+
* Creates a container from a layer.
|
|
737
631
|
*
|
|
738
|
-
* This
|
|
739
|
-
*
|
|
632
|
+
* This is a convenience method equivalent to applying a layer to
|
|
633
|
+
* `Container.builder()` and building the result.
|
|
740
634
|
*
|
|
741
|
-
* @param
|
|
742
|
-
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
635
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
743
636
|
*
|
|
744
637
|
* @example
|
|
745
638
|
* ```typescript
|
|
746
|
-
* const
|
|
747
|
-
*
|
|
748
|
-
* ```
|
|
749
|
-
*/
|
|
750
|
-
has(tag: AnyTag): boolean;
|
|
751
|
-
/**
|
|
752
|
-
* Checks if a dependency has been instantiated (cached) in the container.
|
|
639
|
+
* const dbLayer = Layer.service(Database, []);
|
|
640
|
+
* const container = Container.from(dbLayer);
|
|
753
641
|
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
642
|
+
* const db = await container.resolve(Database);
|
|
643
|
+
* ```
|
|
756
644
|
*/
|
|
757
|
-
|
|
645
|
+
static from<TProvides extends AnyTag>(layer: Layer<never, TProvides>): Container<TProvides>;
|
|
758
646
|
/**
|
|
759
|
-
*
|
|
647
|
+
* Resolves a dependency, creating it if necessary.
|
|
760
648
|
*
|
|
761
|
-
*
|
|
762
|
-
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
763
|
-
* requests for the same dependency correctly.
|
|
649
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
764
650
|
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* @
|
|
769
|
-
* @param tag - The dependency tag to retrieve
|
|
770
|
-
* @returns Promise resolving to the service instance
|
|
771
|
-
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
651
|
+
* @param tag - The dependency tag to resolve
|
|
652
|
+
* @returns Promise resolving to the dependency instance
|
|
653
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
654
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
772
655
|
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
773
|
-
* @throws {DependencyCreationError} If
|
|
774
|
-
*
|
|
775
|
-
* @example Basic usage
|
|
776
|
-
* ```typescript
|
|
777
|
-
* const container = Container.empty()
|
|
778
|
-
* .register(DatabaseService, () => new DatabaseService());
|
|
779
|
-
*
|
|
780
|
-
* const db = await c.resolve(DatabaseService);
|
|
781
|
-
* db.query('SELECT * FROM users');
|
|
782
|
-
* ```
|
|
783
|
-
*
|
|
784
|
-
* @example Concurrent access (singleton behavior)
|
|
785
|
-
* ```typescript
|
|
786
|
-
* // All three calls will receive the same instance
|
|
787
|
-
* const [db1, db2, db3] = await Promise.all([
|
|
788
|
-
* c.resolve(DatabaseService),
|
|
789
|
-
* c.resolve(DatabaseService),
|
|
790
|
-
* c.resolve(DatabaseService)
|
|
791
|
-
* ]);
|
|
792
|
-
*
|
|
793
|
-
* console.log(db1 === db2 === db3); // true
|
|
794
|
-
* ```
|
|
795
|
-
*
|
|
796
|
-
* @example Dependency injection in factories
|
|
797
|
-
* ```typescript
|
|
798
|
-
* const container = Container.empty()
|
|
799
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
800
|
-
* .register(UserService, async (ctx) => {
|
|
801
|
-
* const db = await ctx.resolve(DatabaseService);
|
|
802
|
-
* return new UserService(db);
|
|
803
|
-
* });
|
|
804
|
-
*
|
|
805
|
-
* const userService = await c.resolve(UserService);
|
|
806
|
-
* ```
|
|
656
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
807
657
|
*/
|
|
808
658
|
resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
|
|
809
659
|
/**
|
|
810
|
-
* Internal resolution
|
|
811
|
-
* Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
|
|
660
|
+
* Internal resolution with dependency chain tracking.
|
|
812
661
|
* @internal
|
|
813
662
|
*/
|
|
814
663
|
protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
815
664
|
/**
|
|
816
|
-
* Resolves multiple dependencies concurrently
|
|
817
|
-
*
|
|
818
|
-
* This method takes a variable number of dependency tags and resolves all of them concurrently,
|
|
819
|
-
* returning a tuple with the resolved instances in the same order as the input tags.
|
|
820
|
-
* The method maintains all the same guarantees as the individual resolve method:
|
|
821
|
-
* singleton behavior, circular dependency detection, and proper error handling.
|
|
665
|
+
* Resolves multiple dependencies concurrently.
|
|
822
666
|
*
|
|
823
|
-
* @
|
|
824
|
-
* @
|
|
825
|
-
* @returns Promise resolving to a tuple of service instances in the same order
|
|
667
|
+
* @param tags - The dependency tags to resolve
|
|
668
|
+
* @returns Promise resolving to a tuple of instances
|
|
826
669
|
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
827
670
|
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
828
671
|
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
829
672
|
* @throws {DependencyCreationError} If any factory function throws an error
|
|
830
|
-
*
|
|
831
|
-
* @example Basic usage
|
|
832
|
-
* ```typescript
|
|
833
|
-
* const container = Container.empty()
|
|
834
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
835
|
-
* .register(LoggerService, () => new LoggerService());
|
|
836
|
-
*
|
|
837
|
-
* const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
|
|
838
|
-
* ```
|
|
839
|
-
*
|
|
840
|
-
* @example Mixed tag types
|
|
841
|
-
* ```typescript
|
|
842
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
843
|
-
* const container = Container.empty()
|
|
844
|
-
* .register(ApiKeyTag, () => 'secret-key')
|
|
845
|
-
* .register(UserService, () => new UserService());
|
|
846
|
-
*
|
|
847
|
-
* const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
|
|
848
|
-
* ```
|
|
849
|
-
*
|
|
850
|
-
* @example Empty array
|
|
851
|
-
* ```typescript
|
|
852
|
-
* const results = await c.resolveAll(); // Returns empty array
|
|
853
|
-
* ```
|
|
854
673
|
*/
|
|
855
674
|
resolveAll<const T extends readonly TTags[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
856
675
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
860
|
-
* Any subsequent calls to register(), get(), or destroy() will throw a DependencyFinalizationError.
|
|
861
|
-
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
862
|
-
*
|
|
863
|
-
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
864
|
-
* for maximum cleanup performance.
|
|
865
|
-
* If any finalizers fail, all errors are collected and a DependencyFinalizationError
|
|
866
|
-
* is thrown containing details of all failures.
|
|
676
|
+
* Resolves a service, runs the callback with it, then destroys the container.
|
|
867
677
|
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* dependencies are cleaned up.
|
|
678
|
+
* This is a convenience method for the common "create, use, destroy" pattern.
|
|
679
|
+
* The container is always destroyed after the callback completes, even if it throws.
|
|
871
680
|
*
|
|
872
|
-
* @
|
|
873
|
-
* @
|
|
874
|
-
*
|
|
875
|
-
* @
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
* const conn = new DatabaseConnection();
|
|
881
|
-
* await conn.connect();
|
|
882
|
-
* return conn;
|
|
883
|
-
* },
|
|
884
|
-
* (conn) => conn.disconnect() // Finalizer
|
|
885
|
-
* );
|
|
886
|
-
*
|
|
887
|
-
* const db = await c.resolve(DatabaseConnection);
|
|
888
|
-
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
889
|
-
*
|
|
890
|
-
* // This will throw an error
|
|
891
|
-
* try {
|
|
892
|
-
* await c.resolve(DatabaseConnection);
|
|
893
|
-
* } catch (error) {
|
|
894
|
-
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
895
|
-
* }
|
|
896
|
-
* ```
|
|
897
|
-
*
|
|
898
|
-
* @example Application shutdown
|
|
899
|
-
* ```typescript
|
|
900
|
-
* const appContainer Container.empty
|
|
901
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
902
|
-
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
|
|
903
|
-
*
|
|
904
|
-
* // During application shutdown
|
|
905
|
-
* process.on('SIGTERM', async () => {
|
|
906
|
-
* try {
|
|
907
|
-
* await appContainer.destroy(); // Clean shutdown of all services
|
|
908
|
-
* } catch (error) {
|
|
909
|
-
* console.error('Error during shutdown:', error);
|
|
910
|
-
* }
|
|
911
|
-
* process.exit(0);
|
|
912
|
-
* });
|
|
913
|
-
* ```
|
|
681
|
+
* @param tag - The dependency tag to resolve
|
|
682
|
+
* @param fn - Callback that receives the resolved service
|
|
683
|
+
* @returns Promise resolving to the callback's return value
|
|
684
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
685
|
+
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
686
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
687
|
+
* @throws {DependencyCreationError} If the factory function throws
|
|
688
|
+
* @throws {DependencyFinalizationError} If the finalizer function throws
|
|
914
689
|
*
|
|
915
|
-
* @example
|
|
690
|
+
* @example
|
|
916
691
|
* ```typescript
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
922
|
-
* }
|
|
923
|
-
* }
|
|
924
|
-
* // Container is destroyed regardless of finalizer errors
|
|
692
|
+
* const result = await container.use(UserService, (service) =>
|
|
693
|
+
* service.getUsers()
|
|
694
|
+
* );
|
|
695
|
+
* // Container is automatically destroyed after callback completes
|
|
925
696
|
* ```
|
|
926
697
|
*/
|
|
927
|
-
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
//#endregion
|
|
931
|
-
//#region src/scoped-container.d.ts
|
|
932
|
-
type Scope = string | symbol;
|
|
933
|
-
declare class ScopedContainer<TTags extends AnyTag> extends Container<TTags> {
|
|
934
|
-
readonly scope: Scope;
|
|
935
|
-
private parent;
|
|
936
|
-
private readonly children;
|
|
937
|
-
protected constructor(parent: IContainer<TTags> | null, scope: Scope);
|
|
938
|
-
/**
|
|
939
|
-
* Creates a new empty scoped container instance.
|
|
940
|
-
* @param scope - The scope identifier for this container
|
|
941
|
-
* @returns A new empty ScopedContainer instance with no registered dependencies
|
|
942
|
-
*/
|
|
943
|
-
static empty(scope: Scope): ScopedContainer<never>;
|
|
944
|
-
/**
|
|
945
|
-
* Registers a dependency in the scoped container.
|
|
946
|
-
*
|
|
947
|
-
* Overrides the base implementation to return ScopedContainer type
|
|
948
|
-
* for proper method chaining support.
|
|
949
|
-
*/
|
|
950
|
-
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainer<TTags | T>;
|
|
951
|
-
/**
|
|
952
|
-
* Checks if a dependency has been registered in this scope or any parent scope.
|
|
953
|
-
*
|
|
954
|
-
* This method checks the current scope first, then walks up the parent chain.
|
|
955
|
-
* Returns true if the dependency has been registered somewhere in the scope hierarchy.
|
|
956
|
-
*/
|
|
957
|
-
has(tag: AnyTag): boolean;
|
|
958
|
-
/**
|
|
959
|
-
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
960
|
-
*
|
|
961
|
-
* This method checks the current scope first, then walks up the parent chain.
|
|
962
|
-
* Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
|
|
963
|
-
*/
|
|
964
|
-
exists(tag: AnyTag): boolean;
|
|
965
|
-
/**
|
|
966
|
-
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
967
|
-
*
|
|
968
|
-
* Resolution strategy:
|
|
969
|
-
* 1. Check cache in current scope
|
|
970
|
-
* 2. Check if factory exists in current scope - if so, create instance here
|
|
971
|
-
* 3. Otherwise, delegate to parent scope
|
|
972
|
-
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
973
|
-
*/
|
|
974
|
-
resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
|
|
975
|
-
/**
|
|
976
|
-
* Internal resolution with delegation logic for scoped containers.
|
|
977
|
-
* @internal
|
|
978
|
-
*/
|
|
979
|
-
protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
698
|
+
use<T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>): Promise<R>;
|
|
980
699
|
/**
|
|
981
|
-
* Destroys
|
|
700
|
+
* Destroys the container, calling all finalizers.
|
|
982
701
|
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
702
|
+
* After destruction, the container cannot be used.
|
|
703
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
704
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
705
|
+
* dependencies are cleaned up.
|
|
987
706
|
*
|
|
988
|
-
*
|
|
989
|
-
* before their dependents.
|
|
707
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
990
708
|
*/
|
|
991
709
|
destroy(): Promise<void>;
|
|
992
|
-
/**
|
|
993
|
-
* Creates a child scoped container.
|
|
994
|
-
*
|
|
995
|
-
* Child containers inherit access to parent dependencies but maintain
|
|
996
|
-
* their own scope for new registrations and instance caching.
|
|
997
|
-
*/
|
|
998
|
-
child(scope: Scope): ScopedContainer<TTags>;
|
|
999
710
|
}
|
|
1000
|
-
|
|
1001
|
-
//#endregion
|
|
1002
|
-
//#region src/layer.d.ts
|
|
1003
711
|
/**
|
|
1004
|
-
*
|
|
1005
|
-
* Preserves the concrete container type (Container, ScopedContainer, or IContainer).
|
|
1006
|
-
*
|
|
1007
|
-
* Uses contravariance to detect container types:
|
|
1008
|
-
* - Any ScopedContainer<X> extends ScopedContainer<never>
|
|
1009
|
-
* - Any Container<X> extends Container<never> (but not ScopedContainer<never>)
|
|
1010
|
-
* - Falls back to IContainer for anything else
|
|
1011
|
-
* @internal
|
|
1012
|
-
*/
|
|
1013
|
-
type WithContainerTags<TContainer, TNewTags extends AnyTag> = TContainer extends ScopedContainer<never> ? ScopedContainer<TNewTags> : TContainer extends Container<never> ? Container<TNewTags> : IContainer<TNewTags>;
|
|
1014
|
-
/**
|
|
1015
|
-
* The most generic layer type that accepts any concrete layer.
|
|
712
|
+
* Scope identifier type.
|
|
1016
713
|
*/
|
|
1017
|
-
type
|
|
1018
|
-
/**
|
|
1019
|
-
* The type ID for the Layer interface.
|
|
1020
|
-
*/
|
|
1021
|
-
declare const LayerTypeId: unique symbol;
|
|
714
|
+
type Scope = string | symbol;
|
|
1022
715
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
* Layers allow you to organize your dependency injection setup into logical groups
|
|
1025
|
-
* that can be combined and reused across different contexts.
|
|
1026
|
-
*
|
|
1027
|
-
* ## Type Variance
|
|
1028
|
-
*
|
|
1029
|
-
* The Layer interface uses TypeScript's variance annotations to enable safe substitutability:
|
|
1030
|
-
*
|
|
1031
|
-
* ### TRequires (covariant)
|
|
1032
|
-
* A layer requiring fewer dependencies can substitute one requiring more:
|
|
1033
|
-
* - `Layer<never, X>` can be used where `Layer<A | B, X>` is expected
|
|
1034
|
-
* - Intuition: A service that needs nothing is more flexible than one that needs specific deps
|
|
1035
|
-
*
|
|
1036
|
-
* ### TProvides (contravariant)
|
|
1037
|
-
* A layer providing more services can substitute one providing fewer:
|
|
1038
|
-
* - `Layer<X, A | B>` can be used where `Layer<X, A>` is expected
|
|
1039
|
-
* - Intuition: A service that gives you extra things is compatible with expecting fewer things
|
|
1040
|
-
*
|
|
1041
|
-
* @template TRequires - The union of tags this layer requires to be satisfied by other layers
|
|
1042
|
-
* @template TProvides - The union of tags this layer provides/registers
|
|
1043
|
-
*
|
|
1044
|
-
* @example Basic layer usage
|
|
1045
|
-
* ```typescript
|
|
1046
|
-
* import { layer, Tag, container } from 'sandly';
|
|
1047
|
-
*
|
|
1048
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1049
|
-
* query() { return 'data'; }
|
|
1050
|
-
* }
|
|
1051
|
-
*
|
|
1052
|
-
* // Create a layer that provides DatabaseService
|
|
1053
|
-
* const databaseLayer = layer<never, typeof DatabaseService>((container) =>
|
|
1054
|
-
* container.register(DatabaseService, () => new DatabaseService())
|
|
1055
|
-
* );
|
|
1056
|
-
*
|
|
1057
|
-
* // Apply the layer to a container
|
|
1058
|
-
* const container = Container.empty();
|
|
1059
|
-
* const finalContainer = databaseLayer.register(c);
|
|
1060
|
-
*
|
|
1061
|
-
* const db = await finalContainer.resolve(DatabaseService);
|
|
1062
|
-
* ```
|
|
1063
|
-
*
|
|
1064
|
-
* @example Layer composition with variance
|
|
1065
|
-
* ```typescript
|
|
1066
|
-
* // Layer that requires DatabaseService and provides UserService
|
|
1067
|
-
* const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
|
|
1068
|
-
* container.register(UserService, async (ctx) =>
|
|
1069
|
-
* new UserService(await ctx.resolve(DatabaseService))
|
|
1070
|
-
* )
|
|
1071
|
-
* );
|
|
716
|
+
* Builder for constructing scoped containers.
|
|
1072
717
|
*
|
|
1073
|
-
*
|
|
1074
|
-
* const appLayer = userLayer.provide(databaseLayer);
|
|
1075
|
-
* ```
|
|
718
|
+
* @template TTags - Union type of registered dependency tags
|
|
1076
719
|
*/
|
|
1077
|
-
|
|
1078
|
-
readonly
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
720
|
+
declare class ScopedContainerBuilder<TTags extends AnyTag = never> {
|
|
721
|
+
private readonly scope;
|
|
722
|
+
private readonly parent;
|
|
723
|
+
private readonly factories;
|
|
724
|
+
private readonly finalizers;
|
|
725
|
+
constructor(scope: Scope, parent: IContainer<TTags> | null);
|
|
1082
726
|
/**
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* ## Generic Container Support
|
|
1086
|
-
*
|
|
1087
|
-
* The signature uses `TContainer extends AnyTag` to accept containers with any existing
|
|
1088
|
-
* services while preserving type information. The container must provide at least this
|
|
1089
|
-
* layer's requirements (`TRequires`) but can have additional services (`TContainer`).
|
|
1090
|
-
*
|
|
1091
|
-
* Result container has: `TRequires | TContainer | TProvides` - everything that was
|
|
1092
|
-
* already there plus this layer's new provisions.
|
|
1093
|
-
*
|
|
1094
|
-
* @param container - The container to register dependencies into (must satisfy TRequires)
|
|
1095
|
-
* @returns A new container with this layer's dependencies registered and all existing services preserved
|
|
1096
|
-
*
|
|
1097
|
-
* @example Basic usage
|
|
1098
|
-
* ```typescript
|
|
1099
|
-
* const container = Container.empty();
|
|
1100
|
-
* const updatedContainer = myLayer.register(c);
|
|
1101
|
-
* ```
|
|
1102
|
-
*
|
|
1103
|
-
* @example With existing services preserved
|
|
1104
|
-
* ```typescript
|
|
1105
|
-
* const baseContainer = Container.empty()
|
|
1106
|
-
* .register(ExistingService, () => new ExistingService());
|
|
1107
|
-
*
|
|
1108
|
-
* const enhanced = myLayer.register(baseContainer);
|
|
1109
|
-
* // Enhanced container has both ExistingService and myLayer's provisions
|
|
1110
|
-
* ```
|
|
727
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
1111
728
|
*/
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
*
|
|
1115
|
-
* provisions satisfy this layer's requirements. This creates a dependency flow from dependency → this.
|
|
1116
|
-
*
|
|
1117
|
-
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
1118
|
-
* provisions and any remaining external requirements.
|
|
1119
|
-
*
|
|
1120
|
-
* @template TDepRequires - What the dependency layer requires
|
|
1121
|
-
* @template TDepProvides - What the dependency layer provides
|
|
1122
|
-
* @param dependency - The layer to provide as a dependency
|
|
1123
|
-
* @returns A new composed layer that only exposes this layer's provisions
|
|
1124
|
-
*
|
|
1125
|
-
* @example Simple composition
|
|
1126
|
-
* ```typescript
|
|
1127
|
-
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
1128
|
-
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
1129
|
-
*
|
|
1130
|
-
* // Provide config to database layer
|
|
1131
|
-
* const infraLayer = dbLayer.provide(configLayer);
|
|
1132
|
-
* ```
|
|
1133
|
-
*
|
|
1134
|
-
* @example Multi-level composition (reads naturally left-to-right)
|
|
1135
|
-
* ```typescript
|
|
1136
|
-
* const appLayer = apiLayer
|
|
1137
|
-
* .provide(serviceLayer)
|
|
1138
|
-
* .provide(databaseLayer)
|
|
1139
|
-
* .provide(configLayer);
|
|
1140
|
-
* ```
|
|
1141
|
-
*/
|
|
1142
|
-
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
|
|
1143
|
-
/**
|
|
1144
|
-
* Provides a dependency layer to this layer and merges the provisions.
|
|
1145
|
-
* Unlike `.provide()`, this method includes both this layer's provisions and the dependency layer's
|
|
1146
|
-
* provisions in the result type. This is useful when you want to expose services from both layers.
|
|
1147
|
-
*
|
|
1148
|
-
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
1149
|
-
* provisions and any remaining external requirements.
|
|
1150
|
-
*
|
|
1151
|
-
* @template TDepRequires - What the dependency layer requires
|
|
1152
|
-
* @template TDepProvides - What the dependency layer provides
|
|
1153
|
-
* @param dependency - The layer to provide as a dependency
|
|
1154
|
-
* @returns A new composed layer that provides services from both layers
|
|
1155
|
-
*
|
|
1156
|
-
* @example Providing with merged provisions
|
|
1157
|
-
* ```typescript
|
|
1158
|
-
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
1159
|
-
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
1160
|
-
*
|
|
1161
|
-
* // Provide config to database layer, and both services are available
|
|
1162
|
-
* const infraLayer = dbLayer.provideMerge(configLayer);
|
|
1163
|
-
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
1164
|
-
* ```
|
|
1165
|
-
*
|
|
1166
|
-
* @example Difference from .provide()
|
|
1167
|
-
* ```typescript
|
|
1168
|
-
* // .provide() only exposes this layer's provisions:
|
|
1169
|
-
* const withProvide = dbLayer.provide(configLayer);
|
|
1170
|
-
* // Type: Layer<never, typeof DatabaseService>
|
|
1171
|
-
*
|
|
1172
|
-
* // .provideMerge() exposes both layers' provisions:
|
|
1173
|
-
* const withProvideMerge = dbLayer.provideMerge(configLayer);
|
|
1174
|
-
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
1175
|
-
* ```
|
|
1176
|
-
*/
|
|
1177
|
-
provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
|
|
1178
|
-
/**
|
|
1179
|
-
* Merges this layer with another layer, combining their requirements and provisions.
|
|
1180
|
-
* This is useful for combining independent layers that don't have a dependency
|
|
1181
|
-
* relationship.
|
|
1182
|
-
*
|
|
1183
|
-
* @template TOtherRequires - What the other layer requires
|
|
1184
|
-
* @template TOtherProvides - What the other layer provides
|
|
1185
|
-
* @param other - The layer to merge with
|
|
1186
|
-
* @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
|
|
1187
|
-
*
|
|
1188
|
-
* @example Merging independent layers
|
|
1189
|
-
* ```typescript
|
|
1190
|
-
* const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
|
|
1191
|
-
* const loggingLayer = layer<never, typeof LoggerService>(...);
|
|
1192
|
-
*
|
|
1193
|
-
* // Combine infrastructure layers
|
|
1194
|
-
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
1195
|
-
* ```
|
|
1196
|
-
*
|
|
1197
|
-
* @example Building complex layer combinations
|
|
1198
|
-
* ```typescript
|
|
1199
|
-
* const appInfraLayer = persistenceLayer
|
|
1200
|
-
* .merge(messagingLayer)
|
|
1201
|
-
* .merge(observabilityLayer);
|
|
1202
|
-
* ```
|
|
729
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainerBuilder<TTags | T>;
|
|
730
|
+
/**
|
|
731
|
+
* Creates an immutable scoped container from the registered dependencies.
|
|
1203
732
|
*/
|
|
1204
|
-
|
|
733
|
+
build(): ScopedContainer<TTags>;
|
|
1205
734
|
}
|
|
1206
735
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
1209
|
-
*
|
|
1210
|
-
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
1211
|
-
* @template TProvides - The union of dependency tags this layer registers/provides
|
|
1212
|
-
*
|
|
1213
|
-
* @param register - Function that performs the dependency registrations. Receives a container.
|
|
1214
|
-
* @returns The layer instance.
|
|
1215
|
-
*
|
|
1216
|
-
* @example Simple layer
|
|
1217
|
-
* ```typescript
|
|
1218
|
-
* import { layer, Tag } from 'sandly';
|
|
1219
|
-
*
|
|
1220
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1221
|
-
* constructor(private url: string = 'sqlite://memory') {}
|
|
1222
|
-
* query() { return 'data'; }
|
|
1223
|
-
* }
|
|
736
|
+
* Scoped container for hierarchical dependency management.
|
|
1224
737
|
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
* );
|
|
738
|
+
* Supports parent/child relationships where children can access parent
|
|
739
|
+
* dependencies but maintain their own cache. Useful for request-scoped
|
|
740
|
+
* dependencies in web applications.
|
|
1229
741
|
*
|
|
1230
|
-
*
|
|
1231
|
-
* const dbLayerInstance = databaseLayer;
|
|
1232
|
-
* ```
|
|
742
|
+
* @template TTags - Union type of registered dependency tags
|
|
1233
743
|
*
|
|
1234
|
-
* @example
|
|
744
|
+
* @example
|
|
1235
745
|
* ```typescript
|
|
1236
|
-
* //
|
|
1237
|
-
* const
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
* //
|
|
1242
|
-
* const
|
|
1243
|
-
* (
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* );
|
|
1248
|
-
*
|
|
1249
|
-
* // Service layer (requires infrastructure)
|
|
1250
|
-
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
1251
|
-
* (container) =>
|
|
1252
|
-
* container.register(UserService, async (ctx) =>
|
|
1253
|
-
* new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
|
|
1254
|
-
* )
|
|
1255
|
-
* );
|
|
1256
|
-
*
|
|
1257
|
-
* // Compose the complete application
|
|
1258
|
-
* const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
|
|
746
|
+
* // Application-level container
|
|
747
|
+
* const appContainer = ScopedContainer.builder('app')
|
|
748
|
+
* .add(Database, () => new Database())
|
|
749
|
+
* .build();
|
|
750
|
+
*
|
|
751
|
+
* // Request-level container (inherits from app)
|
|
752
|
+
* const requestContainer = appContainer.child('request')
|
|
753
|
+
* .add(RequestContext, () => new RequestContext())
|
|
754
|
+
* .build();
|
|
755
|
+
*
|
|
756
|
+
* // Can resolve both app and request dependencies
|
|
757
|
+
* const db = await requestContainer.resolve(Database);
|
|
758
|
+
* const ctx = await requestContainer.resolve(RequestContext);
|
|
1259
759
|
* ```
|
|
1260
760
|
*/
|
|
1261
|
-
declare
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
*
|
|
1270
|
-
* @internal
|
|
1271
|
-
*/
|
|
1272
|
-
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
|
|
1273
|
-
/**
|
|
1274
|
-
* Helper type that extracts the union of all provisions from an array of layers.
|
|
1275
|
-
* Used by Layer.mergeAll() to compute the correct provision type for the merged layer.
|
|
1276
|
-
*
|
|
1277
|
-
* Works with AnyLayer[] constraint which accepts any concrete layer through variance:
|
|
1278
|
-
* - Layer<X, never> → extracts `never` (no provisions)
|
|
1279
|
-
* - Layer<Y, A | B> → extracts `A | B` (specific provisions)
|
|
1280
|
-
*
|
|
1281
|
-
* @internal
|
|
1282
|
-
*/
|
|
1283
|
-
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
|
|
1284
|
-
/**
|
|
1285
|
-
* Utility object containing helper functions for working with layers.
|
|
1286
|
-
*/
|
|
1287
|
-
declare const Layer: {
|
|
761
|
+
declare class ScopedContainer<TTags extends AnyTag = never> extends Container<TTags> {
|
|
762
|
+
readonly scope: Scope;
|
|
763
|
+
private parent;
|
|
764
|
+
private readonly children;
|
|
765
|
+
/**
|
|
766
|
+
* @internal
|
|
767
|
+
*/
|
|
768
|
+
protected constructor(scope: Scope, parent: IContainer<TTags> | null, factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
|
|
1288
769
|
/**
|
|
1289
|
-
*
|
|
1290
|
-
|
|
770
|
+
* @internal - Used by ScopedContainerBuilder
|
|
771
|
+
*/
|
|
772
|
+
static _createScopedFromBuilder<T extends AnyTag>(scope: Scope, parent: IContainer<T> | null, factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): ScopedContainer<T>;
|
|
773
|
+
/**
|
|
774
|
+
* Creates a new scoped container builder.
|
|
1291
775
|
*
|
|
1292
|
-
* @
|
|
776
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
1293
777
|
*
|
|
1294
778
|
* @example
|
|
1295
779
|
* ```typescript
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
* const appLayer = baseLayer
|
|
1300
|
-
* .merge(configLayer)
|
|
1301
|
-
* .merge(serviceLayer);
|
|
780
|
+
* const container = ScopedContainer.builder('app')
|
|
781
|
+
* .add(Database, () => new Database())
|
|
782
|
+
* .build();
|
|
1302
783
|
* ```
|
|
1303
784
|
*/
|
|
1304
|
-
|
|
785
|
+
static builder(scope: Scope): ScopedContainerBuilder;
|
|
1305
786
|
/**
|
|
1306
|
-
*
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
787
|
+
* Creates an empty scoped container with no dependencies.
|
|
788
|
+
*/
|
|
789
|
+
static empty(scope: Scope): ScopedContainer;
|
|
790
|
+
/**
|
|
791
|
+
* Creates a scoped container from a layer.
|
|
1310
792
|
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
793
|
+
* This is a convenience method equivalent to applying a layer to
|
|
794
|
+
* `ScopedContainer.builder()` and building the result.
|
|
1313
795
|
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* - **Covariant TProvides**: Layer<Y, typeof ServiceB> can be passed because providing
|
|
1317
|
-
* ServiceB is compatible with the general `AnyTag` type
|
|
796
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
797
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
1318
798
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```typescript
|
|
801
|
+
* const dbLayer = Layer.service(Database, []);
|
|
802
|
+
* const container = ScopedContainer.from('app', dbLayer);
|
|
1321
803
|
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
|
|
804
|
+
* const db = await container.resolve(Database);
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
static from<TProvides extends AnyTag>(scope: Scope, layer: Layer<never, TProvides>): ScopedContainer<TProvides>;
|
|
808
|
+
/**
|
|
809
|
+
* Resolves a dependency from this scope or parent scopes, creating it if necessary.
|
|
1325
810
|
*
|
|
1326
|
-
*
|
|
1327
|
-
* @param layers - At least 2 layers to merge together
|
|
1328
|
-
* @returns A new layer that combines all input layers with correct union types
|
|
811
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
1329
812
|
*
|
|
1330
|
-
* @
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
813
|
+
* @param tag - The dependency tag to resolve
|
|
814
|
+
* @returns Promise resolving to the dependency instance
|
|
815
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
816
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
817
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
818
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
819
|
+
*/
|
|
820
|
+
resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
|
|
821
|
+
/**
|
|
822
|
+
* Internal resolution with parent delegation.
|
|
823
|
+
* @internal
|
|
824
|
+
*/
|
|
825
|
+
protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
826
|
+
/**
|
|
827
|
+
* @internal - Used by ScopedContainerBuilder to register children
|
|
828
|
+
*/
|
|
829
|
+
_registerChild(child: ScopedContainer<TTags>): void;
|
|
830
|
+
/**
|
|
831
|
+
* Creates a child container builder that inherits from this container.
|
|
1333
832
|
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
* const userLayer = layer<typeof DatabaseService, typeof UserService>(...); // requires DB
|
|
1337
|
-
* const configLayer = layer<never, typeof ConfigService>(...); // no requirements
|
|
833
|
+
* Use this to create a child scope and add dependencies to it.
|
|
834
|
+
* The child can resolve dependencies from this container.
|
|
1338
835
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
836
|
+
* @param scope - Identifier for the child scope
|
|
837
|
+
* @returns A new ScopedContainerBuilder for the child scope
|
|
838
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
1342
839
|
*
|
|
1343
|
-
* @example
|
|
840
|
+
* @example
|
|
1344
841
|
* ```typescript
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
842
|
+
* const requestContainer = appContainer.child('request')
|
|
843
|
+
* .add(RequestContext, () => new RequestContext())
|
|
844
|
+
* .build();
|
|
845
|
+
*
|
|
846
|
+
* await requestContainer.resolve(Database); // From parent
|
|
847
|
+
* await requestContainer.resolve(RequestContext); // From this scope
|
|
1348
848
|
* ```
|
|
849
|
+
*/
|
|
850
|
+
child(scope: Scope): ScopedContainerBuilder<TTags>;
|
|
851
|
+
/**
|
|
852
|
+
* Creates a child container with a layer applied.
|
|
1349
853
|
*
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
* const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
|
|
1353
|
-
* const messagingLayer = layer<never, typeof MessageQueue>(...);
|
|
1354
|
-
* const observabilityLayer = layer<never, typeof Logger | typeof Metrics>(...);
|
|
854
|
+
* This is a convenience method combining child() + layer.apply() + build().
|
|
855
|
+
* Use this when you have a layer ready to apply.
|
|
1355
856
|
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
857
|
+
* @param scope - Identifier for the child scope
|
|
858
|
+
* @param layer - Layer to apply to the child (can require parent's tags)
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```typescript
|
|
862
|
+
* const requestContainer = appContainer.childFrom('request',
|
|
863
|
+
* userService
|
|
864
|
+
* .provide(Layer.value(TenantContext, tenantCtx))
|
|
865
|
+
* .provide(Layer.value(RequestId, requestId))
|
|
1361
866
|
* );
|
|
1362
867
|
*
|
|
1363
|
-
*
|
|
868
|
+
* const users = await requestContainer.resolve(UserService);
|
|
1364
869
|
* ```
|
|
1365
870
|
*/
|
|
1366
|
-
|
|
871
|
+
childFrom<TProvides extends AnyTag>(scope: Scope, layer: Layer<TTags, TProvides>): ScopedContainer<TTags | TProvides>;
|
|
1367
872
|
/**
|
|
1368
|
-
*
|
|
1369
|
-
* This is similar to the `.merge()` method but available as a static function.
|
|
1370
|
-
*
|
|
1371
|
-
* @template TRequires1 - What the first layer requires
|
|
1372
|
-
* @template TProvides1 - What the first layer provides
|
|
1373
|
-
* @template TRequires2 - What the second layer requires
|
|
1374
|
-
* @template TProvides2 - What the second layer provides
|
|
1375
|
-
* @param layer1 - The first layer to merge
|
|
1376
|
-
* @param layer2 - The second layer to merge
|
|
1377
|
-
* @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
|
|
1378
|
-
*
|
|
1379
|
-
* @example Merging two layers
|
|
1380
|
-
* ```typescript
|
|
1381
|
-
* import { Layer } from 'sandly';
|
|
873
|
+
* Destroys this container and all child containers.
|
|
1382
874
|
*
|
|
1383
|
-
*
|
|
1384
|
-
* const cacheLayer = layer<never, typeof CacheService>(...);
|
|
875
|
+
* Children are destroyed first to ensure proper cleanup order.
|
|
1385
876
|
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
877
|
+
* After destruction, the container cannot be used.
|
|
878
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
879
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
880
|
+
* dependencies are cleaned up.
|
|
881
|
+
*
|
|
882
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
1389
883
|
*/
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
//#endregion
|
|
1394
|
-
//#region src/constant.d.ts
|
|
1395
|
-
/**
|
|
1396
|
-
* Creates a layer that provides a constant value for a given tag.
|
|
1397
|
-
*
|
|
1398
|
-
* @param tag - The value tag to provide
|
|
1399
|
-
* @param constantValue - The constant value to provide
|
|
1400
|
-
* @returns A layer with no dependencies that provides the constant value
|
|
1401
|
-
*
|
|
1402
|
-
* @example
|
|
1403
|
-
* ```typescript
|
|
1404
|
-
* const ApiKey = Tag.of('ApiKey')<string>();
|
|
1405
|
-
* const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
|
|
1406
|
-
*
|
|
1407
|
-
* const apiKey = constant(ApiKey, 'my-secret-key');
|
|
1408
|
-
* const dbUrl = constant(DatabaseUrl, 'postgresql://localhost:5432/myapp');
|
|
1409
|
-
*
|
|
1410
|
-
* const config = Layer.merge(apiKey, dbUrl);
|
|
1411
|
-
* ```
|
|
1412
|
-
*/
|
|
1413
|
-
declare function constant<T extends ValueTag<TagId, unknown>>(tag: T, constantValue: TagType<T>): Layer<never, T>;
|
|
884
|
+
destroy(): Promise<void>;
|
|
885
|
+
}
|
|
1414
886
|
|
|
1415
887
|
//#endregion
|
|
1416
|
-
//#region src/
|
|
1417
|
-
/**
|
|
1418
|
-
* Extracts a union type from a tuple of tags.
|
|
1419
|
-
* Returns `never` for empty arrays.
|
|
1420
|
-
* @internal
|
|
1421
|
-
*/
|
|
1422
|
-
type TagsToUnion<T extends readonly AnyTag[]> = T[number];
|
|
888
|
+
//#region src/errors.d.ts
|
|
1423
889
|
/**
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
* This is a simplified alternative to `layer()` for the common case of defining
|
|
1427
|
-
* a single dependency. Unlike `service()` and `autoService()`, this works with
|
|
1428
|
-
* any tag type (ServiceTag or ValueTag) and doesn't require extending `Tag.Service()`.
|
|
1429
|
-
*
|
|
1430
|
-
* Requirements are passed as an optional array of tags, allowing TypeScript to infer
|
|
1431
|
-
* both the tag type and the requirements automatically - no explicit type
|
|
1432
|
-
* parameters needed.
|
|
1433
|
-
*
|
|
1434
|
-
* @param tag - The tag (ServiceTag or ValueTag) that identifies this dependency
|
|
1435
|
-
* @param spec - Factory function or lifecycle object for creating the dependency
|
|
1436
|
-
* @param requirements - Optional array of dependency tags this dependency requires (defaults to [])
|
|
1437
|
-
* @returns A layer that requires the specified dependencies and provides the tag
|
|
1438
|
-
*
|
|
1439
|
-
* @example Simple dependency without requirements
|
|
1440
|
-
* ```typescript
|
|
1441
|
-
* const Config = Tag.of('Config')<{ apiUrl: string }>();
|
|
1442
|
-
*
|
|
1443
|
-
* // No requirements - can omit the array
|
|
1444
|
-
* const configDep = dependency(Config, () => ({
|
|
1445
|
-
* apiUrl: process.env.API_URL!
|
|
1446
|
-
* }));
|
|
1447
|
-
* ```
|
|
1448
|
-
*
|
|
1449
|
-
* @example Dependency with requirements
|
|
1450
|
-
* ```typescript
|
|
1451
|
-
* const database = dependency(
|
|
1452
|
-
* Database,
|
|
1453
|
-
* async (ctx) => {
|
|
1454
|
-
* const config = await ctx.resolve(Config);
|
|
1455
|
-
* const logger = await ctx.resolve(Logger);
|
|
1456
|
-
* logger.info('Creating database connection');
|
|
1457
|
-
* return createDb(config.DATABASE);
|
|
1458
|
-
* },
|
|
1459
|
-
* [Config, Logger]
|
|
1460
|
-
* );
|
|
1461
|
-
* ```
|
|
1462
|
-
*
|
|
1463
|
-
* @example Dependency with lifecycle (create + cleanup)
|
|
1464
|
-
* ```typescript
|
|
1465
|
-
* const database = dependency(
|
|
1466
|
-
* Database,
|
|
1467
|
-
* {
|
|
1468
|
-
* create: async (ctx) => {
|
|
1469
|
-
* const config = await ctx.resolve(Config);
|
|
1470
|
-
* const logger = await ctx.resolve(Logger);
|
|
1471
|
-
* logger.info('Creating database connection');
|
|
1472
|
-
* return await createDb(config.DATABASE);
|
|
1473
|
-
* },
|
|
1474
|
-
* cleanup: async (db) => {
|
|
1475
|
-
* await disconnectDb(db);
|
|
1476
|
-
* },
|
|
1477
|
-
* },
|
|
1478
|
-
* [Config, Logger]
|
|
1479
|
-
* );
|
|
1480
|
-
* ```
|
|
1481
|
-
*
|
|
1482
|
-
* @example Comparison with layer()
|
|
1483
|
-
* ```typescript
|
|
1484
|
-
* // Using layer() - verbose, requires explicit type parameters
|
|
1485
|
-
* const database = layer<typeof Config | typeof Logger, typeof Database>(
|
|
1486
|
-
* (container) =>
|
|
1487
|
-
* container.register(Database, async (ctx) => {
|
|
1488
|
-
* const config = await ctx.resolve(Config);
|
|
1489
|
-
* return createDb(config.DATABASE);
|
|
1490
|
-
* })
|
|
1491
|
-
* );
|
|
1492
|
-
*
|
|
1493
|
-
* // Using dependency() - cleaner, fully inferred types
|
|
1494
|
-
* const database = dependency(
|
|
1495
|
-
* Database,
|
|
1496
|
-
* async (ctx) => {
|
|
1497
|
-
* const config = await ctx.resolve(Config);
|
|
1498
|
-
* return createDb(config.DATABASE);
|
|
1499
|
-
* },
|
|
1500
|
-
* [Config, Logger]
|
|
1501
|
-
* );
|
|
1502
|
-
* ```
|
|
890
|
+
* Structured error information for debugging and logging.
|
|
1503
891
|
*/
|
|
1504
|
-
declare function dependency<TTag extends AnyTag, TRequirements extends readonly AnyTag[] = []>(tag: TTag, spec: DependencySpec<TTag, TagsToUnion<TRequirements>>, requirements?: TRequirements): Layer<TagsToUnion<TRequirements>, TTag>;
|
|
1505
|
-
|
|
1506
|
-
//#endregion
|
|
1507
|
-
//#region src/errors.d.ts
|
|
1508
892
|
type ErrorDump = {
|
|
1509
893
|
name: string;
|
|
1510
894
|
message: string;
|
|
@@ -1512,17 +896,20 @@ type ErrorDump = {
|
|
|
1512
896
|
detail: Record<string, unknown>;
|
|
1513
897
|
cause?: unknown;
|
|
1514
898
|
};
|
|
899
|
+
/**
|
|
900
|
+
* Options for creating Sandly errors.
|
|
901
|
+
*/
|
|
1515
902
|
type SandlyErrorOptions = {
|
|
1516
903
|
cause?: unknown;
|
|
1517
904
|
detail?: Record<string, unknown>;
|
|
1518
905
|
};
|
|
1519
906
|
/**
|
|
1520
|
-
* Base error class for all library errors.
|
|
907
|
+
* Base error class for all Sandly library errors.
|
|
1521
908
|
*
|
|
1522
|
-
*
|
|
909
|
+
* Extends the native Error class to provide consistent error handling
|
|
1523
910
|
* and structured error information across the library.
|
|
1524
911
|
*
|
|
1525
|
-
* @example
|
|
912
|
+
* @example
|
|
1526
913
|
* ```typescript
|
|
1527
914
|
* try {
|
|
1528
915
|
* await container.resolve(SomeService);
|
|
@@ -1540,44 +927,36 @@ declare class SandlyError extends Error {
|
|
|
1540
927
|
cause,
|
|
1541
928
|
detail
|
|
1542
929
|
}?: SandlyErrorOptions);
|
|
930
|
+
/**
|
|
931
|
+
* Wraps any error as a SandlyError.
|
|
932
|
+
*/
|
|
1543
933
|
static ensure(error: unknown): SandlyError;
|
|
934
|
+
/**
|
|
935
|
+
* Returns a structured representation of the error for logging.
|
|
936
|
+
*/
|
|
1544
937
|
dump(): ErrorDump;
|
|
938
|
+
/**
|
|
939
|
+
* Returns a JSON string representation of the error.
|
|
940
|
+
*/
|
|
1545
941
|
dumps(): string;
|
|
1546
942
|
/**
|
|
1547
943
|
* Recursively extract cause chain from any Error.
|
|
1548
|
-
* Handles both AppError (with dump()) and plain Errors (with cause property).
|
|
1549
944
|
*/
|
|
1550
945
|
private dumpCause;
|
|
1551
946
|
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
1554
|
-
*
|
|
1555
|
-
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
1556
|
-
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
1557
|
-
* by existing dependencies.
|
|
1558
|
-
*/
|
|
1559
|
-
declare class DependencyAlreadyInstantiatedError extends SandlyError {}
|
|
1560
947
|
/**
|
|
1561
948
|
* Error thrown when attempting to use a container that has been destroyed.
|
|
1562
|
-
*
|
|
1563
|
-
* This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
|
|
1564
|
-
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
1565
|
-
* is being used after it has been destroyed.
|
|
1566
949
|
*/
|
|
1567
950
|
declare class ContainerDestroyedError extends SandlyError {}
|
|
1568
951
|
/**
|
|
1569
952
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
1570
953
|
*
|
|
1571
|
-
* This error occurs when calling `container.resolve(Tag)` for a tag that was never
|
|
1572
|
-
* registered via `container.register()`. It indicates a programming error where
|
|
1573
|
-
* the dependency setup is incomplete.
|
|
1574
|
-
*
|
|
1575
954
|
* @example
|
|
1576
955
|
* ```typescript
|
|
1577
|
-
* const container = Container.
|
|
956
|
+
* const container = Container.builder().build(); // Empty container
|
|
1578
957
|
*
|
|
1579
958
|
* try {
|
|
1580
|
-
* await
|
|
959
|
+
* await container.resolve(UnregisteredService);
|
|
1581
960
|
* } catch (error) {
|
|
1582
961
|
* if (error instanceof UnknownDependencyError) {
|
|
1583
962
|
* console.error('Missing dependency:', error.message);
|
|
@@ -1586,360 +965,81 @@ declare class ContainerDestroyedError extends SandlyError {}
|
|
|
1586
965
|
* ```
|
|
1587
966
|
*/
|
|
1588
967
|
declare class UnknownDependencyError extends SandlyError {
|
|
1589
|
-
/**
|
|
1590
|
-
* @internal
|
|
1591
|
-
* Creates an UnknownDependencyError for the given tag.
|
|
1592
|
-
*
|
|
1593
|
-
* @param tag - The dependency tag that wasn't found
|
|
1594
|
-
*/
|
|
1595
968
|
constructor(tag: AnyTag);
|
|
1596
969
|
}
|
|
1597
970
|
/**
|
|
1598
|
-
* Error thrown when a circular dependency is detected during
|
|
1599
|
-
*
|
|
1600
|
-
* This occurs when service A depends on service B, which depends on service A (directly
|
|
1601
|
-
* or through a chain of dependencies). The error includes the full dependency chain
|
|
1602
|
-
* to help identify the circular reference.
|
|
971
|
+
* Error thrown when a circular dependency is detected during resolution.
|
|
1603
972
|
*
|
|
1604
|
-
* @example
|
|
973
|
+
* @example
|
|
1605
974
|
* ```typescript
|
|
1606
|
-
*
|
|
1607
|
-
* class ServiceB extends Tag.Service('ServiceB') {}
|
|
1608
|
-
*
|
|
1609
|
-
* const container = Container.empty()
|
|
1610
|
-
* .register(ServiceA, async (ctx) =>
|
|
1611
|
-
* new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
|
|
1612
|
-
* )
|
|
1613
|
-
* .register(ServiceB, async (ctx) =>
|
|
1614
|
-
* new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
|
|
1615
|
-
* );
|
|
1616
|
-
*
|
|
975
|
+
* // ServiceA depends on ServiceB, ServiceB depends on ServiceA
|
|
1617
976
|
* try {
|
|
1618
|
-
* await
|
|
977
|
+
* await container.resolve(ServiceA);
|
|
1619
978
|
* } catch (error) {
|
|
1620
979
|
* if (error instanceof CircularDependencyError) {
|
|
1621
980
|
* console.error('Circular dependency:', error.message);
|
|
1622
|
-
* //
|
|
981
|
+
* // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
1623
982
|
* }
|
|
1624
983
|
* }
|
|
1625
984
|
* ```
|
|
1626
985
|
*/
|
|
1627
986
|
declare class CircularDependencyError extends SandlyError {
|
|
1628
|
-
/**
|
|
1629
|
-
* @internal
|
|
1630
|
-
* Creates a CircularDependencyError with the dependency chain information.
|
|
1631
|
-
*
|
|
1632
|
-
* @param tag - The tag where the circular dependency was detected
|
|
1633
|
-
* @param dependencyChain - The chain of dependencies that led to the circular reference
|
|
1634
|
-
*/
|
|
1635
987
|
constructor(tag: AnyTag, dependencyChain: AnyTag[]);
|
|
1636
988
|
}
|
|
1637
989
|
/**
|
|
1638
|
-
* Error thrown when a dependency factory
|
|
1639
|
-
*
|
|
1640
|
-
* This wraps the original error with additional context about which dependency
|
|
1641
|
-
* failed to be created. The original error is preserved as the `cause` property.
|
|
990
|
+
* Error thrown when a dependency factory throws during instantiation.
|
|
1642
991
|
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
992
|
+
* For nested dependencies (A depends on B depends on C), use `getRootCause()`
|
|
993
|
+
* to unwrap all layers and get the original error.
|
|
1645
994
|
*
|
|
1646
|
-
* @example
|
|
995
|
+
* @example
|
|
1647
996
|
* ```typescript
|
|
1648
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
1649
|
-
*
|
|
1650
|
-
* const container = Container.empty().register(DatabaseService, () => {
|
|
1651
|
-
* throw new Error('Database connection failed');
|
|
1652
|
-
* });
|
|
1653
|
-
*
|
|
1654
997
|
* try {
|
|
1655
|
-
* await
|
|
998
|
+
* await container.resolve(UserService);
|
|
1656
999
|
* } catch (error) {
|
|
1657
1000
|
* if (error instanceof DependencyCreationError) {
|
|
1658
1001
|
* console.error('Failed to create:', error.message);
|
|
1659
|
-
* console.error('Original error:', error.cause);
|
|
1660
|
-
* }
|
|
1661
|
-
* }
|
|
1662
|
-
* ```
|
|
1663
|
-
*
|
|
1664
|
-
* @example Getting root cause from nested errors
|
|
1665
|
-
* ```typescript
|
|
1666
|
-
* // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
|
|
1667
|
-
* try {
|
|
1668
|
-
* await container.resolve(ServiceA);
|
|
1669
|
-
* } catch (error) {
|
|
1670
|
-
* if (error instanceof DependencyCreationError) {
|
|
1671
|
-
* console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
|
|
1672
1002
|
* const rootCause = error.getRootCause();
|
|
1673
|
-
* console.error('Root cause:', rootCause);
|
|
1003
|
+
* console.error('Root cause:', rootCause);
|
|
1674
1004
|
* }
|
|
1675
1005
|
* }
|
|
1676
1006
|
* ```
|
|
1677
1007
|
*/
|
|
1678
1008
|
declare class DependencyCreationError extends SandlyError {
|
|
1679
|
-
/**
|
|
1680
|
-
* @internal
|
|
1681
|
-
* Creates a DependencyCreationError wrapping the original factory error.
|
|
1682
|
-
*
|
|
1683
|
-
* @param tag - The tag of the dependency that failed to be created
|
|
1684
|
-
* @param error - The original error thrown by the factory function
|
|
1685
|
-
*/
|
|
1686
1009
|
constructor(tag: AnyTag, error: unknown);
|
|
1687
1010
|
/**
|
|
1688
1011
|
* Traverses the error chain to find the root cause error.
|
|
1689
1012
|
*
|
|
1690
|
-
* When dependencies are nested, each level wraps the error
|
|
1691
|
-
* This method unwraps all
|
|
1692
|
-
*
|
|
1693
|
-
* @returns The root cause error (not a DependencyCreationError unless that's the only error)
|
|
1694
|
-
*
|
|
1695
|
-
* @example
|
|
1696
|
-
* ```typescript
|
|
1697
|
-
* try {
|
|
1698
|
-
* await container.resolve(UserService);
|
|
1699
|
-
* } catch (error) {
|
|
1700
|
-
* if (error instanceof DependencyCreationError) {
|
|
1701
|
-
* const rootCause = error.getRootCause();
|
|
1702
|
-
* console.error('Root cause:', rootCause);
|
|
1703
|
-
* }
|
|
1704
|
-
* }
|
|
1705
|
-
* ```
|
|
1013
|
+
* When dependencies are nested, each level wraps the error.
|
|
1014
|
+
* This method unwraps all layers to get the original error.
|
|
1706
1015
|
*/
|
|
1707
1016
|
getRootCause(): unknown;
|
|
1708
1017
|
}
|
|
1709
1018
|
/**
|
|
1710
1019
|
* Error thrown when one or more finalizers fail during container destruction.
|
|
1711
1020
|
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
* process continues and this error contains details of all failures.
|
|
1021
|
+
* Even if some finalizers fail, cleanup continues for all others.
|
|
1022
|
+
* This error aggregates all failures.
|
|
1715
1023
|
*
|
|
1716
|
-
* @example
|
|
1024
|
+
* @example
|
|
1717
1025
|
* ```typescript
|
|
1718
1026
|
* try {
|
|
1719
1027
|
* await container.destroy();
|
|
1720
1028
|
* } catch (error) {
|
|
1721
1029
|
* if (error instanceof DependencyFinalizationError) {
|
|
1722
|
-
* console.error('
|
|
1723
|
-
* console.error('Error details:', error.detail.errors);
|
|
1030
|
+
* console.error('Cleanup failures:', error.getRootCauses());
|
|
1724
1031
|
* }
|
|
1725
1032
|
* }
|
|
1726
1033
|
* ```
|
|
1727
1034
|
*/
|
|
1728
1035
|
declare class DependencyFinalizationError extends SandlyError {
|
|
1729
1036
|
private readonly errors;
|
|
1730
|
-
/**
|
|
1731
|
-
* @internal
|
|
1732
|
-
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
1733
|
-
*
|
|
1734
|
-
* @param errors - Array of errors thrown by individual finalizers
|
|
1735
|
-
*/
|
|
1736
1037
|
constructor(errors: unknown[]);
|
|
1737
1038
|
/**
|
|
1738
|
-
* Returns
|
|
1739
|
-
*
|
|
1740
|
-
* @returns An array of the errors that occurred during finalization.
|
|
1741
|
-
* You can expect at least one error in the array.
|
|
1039
|
+
* Returns all root cause errors from the finalization failures.
|
|
1742
1040
|
*/
|
|
1743
1041
|
getRootCauses(): unknown[];
|
|
1744
1042
|
}
|
|
1745
1043
|
|
|
1746
1044
|
//#endregion
|
|
1747
|
-
|
|
1748
|
-
/**
|
|
1749
|
-
* Extracts constructor parameter types from a ServiceTag.
|
|
1750
|
-
* Only parameters that extend AnyTag are considered as dependencies.
|
|
1751
|
-
* @internal
|
|
1752
|
-
*/
|
|
1753
|
-
type ConstructorParams<T extends ServiceTag<TagId, unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1754
|
-
/**
|
|
1755
|
-
* Helper to normalize a tag type.
|
|
1756
|
-
* For ServiceTags, this strips away extra static properties of the class constructor,
|
|
1757
|
-
* reducing it to the canonical ServiceTag<Id, Instance> form.
|
|
1758
|
-
* @internal
|
|
1759
|
-
*/
|
|
1760
|
-
type CanonicalTag<T extends AnyTag> = T extends ServiceTag<infer Id, infer Instance> ? ServiceTag<Id, Instance> : T;
|
|
1761
|
-
/**
|
|
1762
|
-
* Extracts only dependency tags from a constructor parameter list.
|
|
1763
|
-
* Filters out non‑DI parameters.
|
|
1764
|
-
*
|
|
1765
|
-
* Example:
|
|
1766
|
-
* [DatabaseService, Inject<typeof ConfigTag>, number]
|
|
1767
|
-
* → typeof DatabaseService | typeof ConfigTag
|
|
1768
|
-
* @internal
|
|
1769
|
-
*/
|
|
1770
|
-
type ExtractConstructorDeps<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1771
|
-
readonly [ServiceTagIdKey]?: infer Id;
|
|
1772
|
-
} ? 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];
|
|
1773
|
-
/**
|
|
1774
|
-
* Produces an ordered tuple of constructor parameters
|
|
1775
|
-
* where dependency parameters are replaced with their tag types,
|
|
1776
|
-
* while non‑DI parameters are preserved as‑is.
|
|
1777
|
-
* @internal
|
|
1778
|
-
*/
|
|
1779
|
-
type InferConstructorDepsTuple<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1780
|
-
readonly [ServiceTagIdKey]?: infer Id;
|
|
1781
|
-
} ? 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]> };
|
|
1782
|
-
/**
|
|
1783
|
-
* Union of all dependency tags a ServiceTag constructor requires.
|
|
1784
|
-
* Filters out non‑DI parameters.
|
|
1785
|
-
*/
|
|
1786
|
-
type ServiceDependencies<T extends ServiceTag<TagId, unknown>> = ExtractConstructorDeps<ConstructorParams<T>> extends AnyTag ? ExtractConstructorDeps<ConstructorParams<T>> : never;
|
|
1787
|
-
/**
|
|
1788
|
-
* Ordered tuple of dependency tags (and other constructor params)
|
|
1789
|
-
* inferred from a ServiceTag’s constructor.
|
|
1790
|
-
*/
|
|
1791
|
-
type ServiceDepsTuple<T extends ServiceTag<TagId, unknown>> = InferConstructorDepsTuple<ConstructorParams<T>>;
|
|
1792
|
-
/**
|
|
1793
|
-
* Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
|
|
1794
|
-
*
|
|
1795
|
-
* For ServiceTag services:
|
|
1796
|
-
* - Dependencies are automatically inferred from constructor parameters
|
|
1797
|
-
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
1798
|
-
*
|
|
1799
|
-
* For ValueTag services:
|
|
1800
|
-
* - No constructor dependencies are needed since they don't have constructors
|
|
1801
|
-
*
|
|
1802
|
-
* @template T - The tag representing the service (ServiceTag or ValueTag)
|
|
1803
|
-
* @param tag - The tag (ServiceTag or ValueTag)
|
|
1804
|
-
* @param factory - Factory function for service instantiation with container
|
|
1805
|
-
* @returns The service layer
|
|
1806
|
-
*
|
|
1807
|
-
* @example Simple service without dependencies
|
|
1808
|
-
* ```typescript
|
|
1809
|
-
* class LoggerService extends Tag.Service('LoggerService') {
|
|
1810
|
-
* log(message: string) { console.log(message); }
|
|
1811
|
-
* }
|
|
1812
|
-
*
|
|
1813
|
-
* const loggerService = service(LoggerService, () => new LoggerService());
|
|
1814
|
-
* ```
|
|
1815
|
-
*
|
|
1816
|
-
* @example Service with dependencies
|
|
1817
|
-
* ```typescript
|
|
1818
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1819
|
-
* query() { return []; }
|
|
1820
|
-
* }
|
|
1821
|
-
*
|
|
1822
|
-
* class UserService extends Tag.Service('UserService') {
|
|
1823
|
-
* constructor(private db: DatabaseService) {
|
|
1824
|
-
* super();
|
|
1825
|
-
* }
|
|
1826
|
-
*
|
|
1827
|
-
* getUsers() { return this.db.query(); }
|
|
1828
|
-
* }
|
|
1829
|
-
*
|
|
1830
|
-
* const userService = service(UserService, async (ctx) =>
|
|
1831
|
-
* new UserService(await ctx.resolve(DatabaseService))
|
|
1832
|
-
* );
|
|
1833
|
-
* ```
|
|
1834
|
-
*/
|
|
1835
|
-
declare function service<T extends ServiceTag<TagId, unknown>>(tag: T, spec: DependencySpec<T, ServiceDependencies<T>>): Layer<ServiceDependencies<T>, CanonicalTag<T>>;
|
|
1836
|
-
/**
|
|
1837
|
-
* Specification for autoService.
|
|
1838
|
-
* Can be either a tuple of constructor parameters or an object with dependencies and finalizer.
|
|
1839
|
-
*/
|
|
1840
|
-
type AutoServiceSpec<T extends ServiceTag<TagId, unknown>> = ServiceDepsTuple<T> | {
|
|
1841
|
-
dependencies: ServiceDepsTuple<T>;
|
|
1842
|
-
cleanup?: Finalizer<TagType<T>>;
|
|
1843
|
-
};
|
|
1844
|
-
/**
|
|
1845
|
-
* Creates a service layer with automatic dependency injection by inferring constructor parameters.
|
|
1846
|
-
*
|
|
1847
|
-
* This is a convenience function that automatically resolves constructor dependencies and passes
|
|
1848
|
-
* both DI-managed dependencies and static values to the service constructor in the correct order.
|
|
1849
|
-
* It eliminates the need to manually write factory functions for services with constructor dependencies.
|
|
1850
|
-
*
|
|
1851
|
-
* @template T - The ServiceTag representing the service class
|
|
1852
|
-
* @param tag - The service tag (must be a ServiceTag, not a ValueTag)
|
|
1853
|
-
* @param deps - Tuple of constructor parameters in order - mix of dependency tags and static values
|
|
1854
|
-
* @param finalizer - Optional cleanup function called when the container is destroyed
|
|
1855
|
-
* @returns A service layer that automatically handles dependency injection
|
|
1856
|
-
*
|
|
1857
|
-
* @example Simple service with dependencies
|
|
1858
|
-
* ```typescript
|
|
1859
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1860
|
-
* constructor(private url: string) {
|
|
1861
|
-
* super();
|
|
1862
|
-
* }
|
|
1863
|
-
* connect() { return `Connected to ${this.url}`; }
|
|
1864
|
-
* }
|
|
1865
|
-
*
|
|
1866
|
-
* class UserService extends Tag.Service('UserService') {
|
|
1867
|
-
* constructor(private db: DatabaseService, private timeout: number) {
|
|
1868
|
-
* super();
|
|
1869
|
-
* }
|
|
1870
|
-
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
1871
|
-
* }
|
|
1872
|
-
*
|
|
1873
|
-
* // Automatically inject DatabaseService and pass static timeout value
|
|
1874
|
-
* const userService = autoService(UserService, [DatabaseService, 5000]);
|
|
1875
|
-
* ```
|
|
1876
|
-
*
|
|
1877
|
-
* @example Mixed dependencies and static values
|
|
1878
|
-
* ```typescript
|
|
1879
|
-
* class NotificationService extends Tag.Service('NotificationService') {
|
|
1880
|
-
* constructor(
|
|
1881
|
-
* private logger: LoggerService,
|
|
1882
|
-
* private apiKey: string,
|
|
1883
|
-
* private retries: number,
|
|
1884
|
-
* private cache: CacheService
|
|
1885
|
-
* ) {
|
|
1886
|
-
* super();
|
|
1887
|
-
* }
|
|
1888
|
-
* }
|
|
1889
|
-
*
|
|
1890
|
-
* // Mix of DI tags and static values in constructor order
|
|
1891
|
-
* const notificationService = autoService(NotificationService, [
|
|
1892
|
-
* LoggerService, // Will be resolved from container
|
|
1893
|
-
* 'secret-api-key', // Static string value
|
|
1894
|
-
* 3, // Static number value
|
|
1895
|
-
* CacheService // Will be resolved from container
|
|
1896
|
-
* ]);
|
|
1897
|
-
* ```
|
|
1898
|
-
*
|
|
1899
|
-
* @example Compared to manual service creation
|
|
1900
|
-
* ```typescript
|
|
1901
|
-
* // Manual approach (more verbose)
|
|
1902
|
-
* const userServiceManual = service(UserService, async (ctx) => {
|
|
1903
|
-
* const db = await ctx.resolve(DatabaseService);
|
|
1904
|
-
* return new UserService(db, 5000);
|
|
1905
|
-
* });
|
|
1906
|
-
*
|
|
1907
|
-
* // Auto approach (concise)
|
|
1908
|
-
* const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
|
|
1909
|
-
* ```
|
|
1910
|
-
*
|
|
1911
|
-
* @example With finalizer for cleanup
|
|
1912
|
-
* ```typescript
|
|
1913
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1914
|
-
* constructor(private connectionString: string) {
|
|
1915
|
-
* super();
|
|
1916
|
-
* }
|
|
1917
|
-
*
|
|
1918
|
-
* private connection: Connection | null = null;
|
|
1919
|
-
*
|
|
1920
|
-
* async connect() {
|
|
1921
|
-
* this.connection = await createConnection(this.connectionString);
|
|
1922
|
-
* }
|
|
1923
|
-
*
|
|
1924
|
-
* async disconnect() {
|
|
1925
|
-
* if (this.connection) {
|
|
1926
|
-
* await this.connection.close();
|
|
1927
|
-
* this.connection = null;
|
|
1928
|
-
* }
|
|
1929
|
-
* }
|
|
1930
|
-
* }
|
|
1931
|
-
*
|
|
1932
|
-
* // Service with automatic cleanup
|
|
1933
|
-
* const dbService = autoService(
|
|
1934
|
-
* DatabaseService,
|
|
1935
|
-
* {
|
|
1936
|
-
* dependencies: ['postgresql://localhost:5432/mydb'],
|
|
1937
|
-
* cleanup: (service) => service.disconnect() // Finalizer for cleanup
|
|
1938
|
-
* }
|
|
1939
|
-
* );
|
|
1940
|
-
* ```
|
|
1941
|
-
*/
|
|
1942
|
-
declare function autoService<T extends ServiceTag<TagId, unknown>>(tag: T, spec: AutoServiceSpec<T>): Layer<ServiceDependencies<T>, CanonicalTag<T>>;
|
|
1943
|
-
|
|
1944
|
-
//#endregion
|
|
1945
|
-
export { AnyLayer, AnyTag, CircularDependencyError, Container, ContainerDestroyedError, ContainerTags, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, DependencyLifecycle, DependencySpec, Factory, Finalizer, IContainer, Inject, InjectSource, Layer, PromiseOrValue, ResolutionContext, SandlyError, Scope, ScopedContainer, ServiceDependencies, ServiceDepsTuple, ServiceTag, Tag, TagType, UnknownDependencyError, ValueTag, autoService, constant, dependency, layer, service };
|
|
1045
|
+
export { AnyLayer, AnyTag, BuilderTags, CircularDependencyError, Container, ContainerBuilder, ContainerDestroyedError, ContainerTags, DependencyCreationError, DependencyFinalizationError, DependencyLifecycle, DependencySpec, ErrorDump, Factory, Finalizer, IContainer, IContainerBuilder, Layer, Layer as LayerInterface, PromiseOrValue, ResolutionContext, SandlyError, Scope, ScopedContainer, ScopedContainerBuilder, ServiceTag, Tag, TagId, TagType, UnknownDependencyError, ValueTag };
|