sandly 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -2624
- package/dist/index.d.ts +645 -1557
- package/dist/index.js +522 -921
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,1510 +1,882 @@
|
|
|
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
|
-
* that must be unique within your application.
|
|
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
|
|
150
93
|
*
|
|
151
|
-
* @
|
|
152
|
-
* @param id - The unique string or symbol identifier for this tag
|
|
153
|
-
* @returns A factory function that creates value tags for the specified type
|
|
154
|
-
*
|
|
155
|
-
* @example Basic usage with strings
|
|
94
|
+
* @example
|
|
156
95
|
* ```typescript
|
|
157
|
-
* const ApiKeyTag = Tag.of('
|
|
158
|
-
* const
|
|
159
|
-
*
|
|
160
|
-
* container
|
|
161
|
-
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
162
|
-
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost', port: 5432 }));
|
|
96
|
+
* const ApiKeyTag = Tag.of('ApiKey')<string>();
|
|
97
|
+
* const PortTag = Tag.of('Port')<number>();
|
|
98
|
+
* const ConfigTag = Tag.of('Config')<{ dbUrl: string; port: number }>();
|
|
163
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.
|
|
104
|
+
*
|
|
105
|
+
* For classes: uses static `Tag` property if present, otherwise `constructor.name`
|
|
106
|
+
* For ValueTags: uses the tag's id
|
|
164
107
|
*
|
|
165
|
-
* @example
|
|
108
|
+
* @example
|
|
166
109
|
* ```typescript
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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"
|
|
169
117
|
*
|
|
170
|
-
*
|
|
118
|
+
* const ConfigTag = Tag.of('app.config')<Config>();
|
|
119
|
+
* Tag.id(ConfigTag); // "app.config"
|
|
171
120
|
* ```
|
|
121
|
+
*/
|
|
122
|
+
id: (tag: AnyTag) => string;
|
|
123
|
+
/**
|
|
124
|
+
* Type guard to check if a value is a ServiceTag (class constructor).
|
|
172
125
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* const PortTag = Tag.of('port')<number>();
|
|
176
|
-
* const EnabledTag = Tag.of('enabled')<boolean>();
|
|
126
|
+
* Returns true for class declarations, class expressions, and regular functions
|
|
127
|
+
* (which can be used as constructors in JavaScript).
|
|
177
128
|
*
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
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.
|
|
182
253
|
*
|
|
183
|
-
* @example
|
|
254
|
+
* @example
|
|
184
255
|
* ```typescript
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
190
264
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* port: 5432,
|
|
195
|
-
* database: 'myapp'
|
|
196
|
-
* }));
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
197
268
|
* ```
|
|
198
269
|
*/
|
|
199
|
-
|
|
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: {
|
|
200
299
|
/**
|
|
201
|
-
* Creates a
|
|
300
|
+
* Creates a layer that provides a class service with automatic dependency injection.
|
|
202
301
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* and the implementation, providing type safety and clear semantics.
|
|
302
|
+
* The dependencies array must match the constructor parameters exactly (order and types).
|
|
303
|
+
* This is validated at compile time.
|
|
206
304
|
*
|
|
207
|
-
* @
|
|
208
|
-
* @param
|
|
209
|
-
* @
|
|
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
|
|
210
308
|
*
|
|
211
|
-
* @example
|
|
309
|
+
* @example
|
|
212
310
|
* ```typescript
|
|
213
|
-
* class UserService
|
|
214
|
-
*
|
|
215
|
-
* return ['alice', 'bob'];
|
|
216
|
-
* }
|
|
311
|
+
* class UserService {
|
|
312
|
+
* constructor(private db: Database, private apiKey: string) {}
|
|
217
313
|
* }
|
|
218
314
|
*
|
|
219
|
-
*
|
|
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']);
|
|
220
322
|
* ```
|
|
323
|
+
*/
|
|
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>;
|
|
327
|
+
/**
|
|
328
|
+
* Creates a layer that provides a constant value.
|
|
221
329
|
*
|
|
222
|
-
* @
|
|
330
|
+
* @param tag - The ValueTag to register
|
|
331
|
+
* @param value - The value to provide
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
223
334
|
* ```typescript
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* }
|
|
335
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
336
|
+
* const ConfigTag = Tag.of('config')<{ port: number }>();
|
|
227
337
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
338
|
+
* const configLayer = Layer.value(ApiKeyTag, 'secret-key')
|
|
339
|
+
* .merge(Layer.value(ConfigTag, { port: 3000 }));
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
value<T extends ValueTag<TagId, unknown>>(tag: T, value: TagType<T>): Layer<never, T>;
|
|
343
|
+
/**
|
|
344
|
+
* Creates a custom layer with full control over the factory logic.
|
|
232
345
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* }
|
|
236
|
-
* }
|
|
346
|
+
* Use this when you need custom instantiation logic that can't be expressed
|
|
347
|
+
* with `Layer.service()` or `Layer.value()`.
|
|
237
348
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* ```
|
|
349
|
+
* - `TRequires` is inferred from the `requires` array
|
|
350
|
+
* - `TProvides` is inferred from what `apply` adds to the builder
|
|
351
|
+
*
|
|
352
|
+
* @param options.requires - Array of tags this layer requires (use [] for no requirements)
|
|
353
|
+
* @param options.apply - Function that adds registrations to a builder
|
|
244
354
|
*
|
|
245
|
-
* @example
|
|
355
|
+
* @example
|
|
246
356
|
* ```typescript
|
|
247
|
-
*
|
|
357
|
+
* // Layer with dependencies - TProvides inferred from builder.add()
|
|
358
|
+
* const cacheLayer = Layer.create({
|
|
359
|
+
* requires: [Database],
|
|
360
|
+
* apply: (builder) => builder
|
|
361
|
+
* .add(Cache, async (ctx) => {
|
|
362
|
+
* const db = await ctx.resolve(Database);
|
|
363
|
+
* return new Cache(db, { ttl: 3600 });
|
|
364
|
+
* })
|
|
365
|
+
* });
|
|
366
|
+
* // Type: Layer<typeof Database, typeof Cache>
|
|
248
367
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
368
|
+
* // Layer with no dependencies
|
|
369
|
+
* const dbLayer = Layer.create({
|
|
370
|
+
* requires: [],
|
|
371
|
+
* apply: (builder) => builder.add(Database, () => new Database())
|
|
372
|
+
* });
|
|
373
|
+
* // Type: Layer<never, typeof Database>
|
|
252
374
|
* ```
|
|
253
375
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
376
|
+
create<const TRequires extends readonly AnyTag[], TAllTags extends AnyTag>(options: {
|
|
377
|
+
requires: TRequires;
|
|
378
|
+
apply: (builder: IContainerBuilder<TRequires[number]>) => IContainerBuilder<TAllTags>;
|
|
379
|
+
}): Layer<TRequires[number], Exclude<TAllTags, TRequires[number]>>;
|
|
257
380
|
/**
|
|
258
|
-
*
|
|
381
|
+
* Creates an empty layer with no requirements or provisions.
|
|
259
382
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```typescript
|
|
385
|
+
* const baseLayer = Layer.empty()
|
|
386
|
+
* .merge(configLayer)
|
|
387
|
+
* .merge(serviceLayer);
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
empty(): Layer<never, never>;
|
|
391
|
+
/**
|
|
392
|
+
* Merges multiple layers at once.
|
|
263
393
|
*
|
|
264
|
-
* @param
|
|
265
|
-
* @returns
|
|
394
|
+
* @param layers - At least 2 layers to merge
|
|
395
|
+
* @returns A layer combining all requirements and provisions
|
|
266
396
|
*
|
|
267
397
|
* @example
|
|
268
398
|
* ```typescript
|
|
269
|
-
* const
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
399
|
+
* const infraLayer = Layer.mergeAll(
|
|
400
|
+
* persistenceLayer,
|
|
401
|
+
* messagingLayer,
|
|
402
|
+
* observabilityLayer
|
|
403
|
+
* );
|
|
274
404
|
* ```
|
|
275
|
-
*
|
|
276
|
-
* @internal - Primarily for internal use in error messages and debugging
|
|
277
405
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
406
|
+
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
407
|
+
/**
|
|
408
|
+
* Merges exactly two layers.
|
|
409
|
+
* Equivalent to `layer1.merge(layer2)`.
|
|
410
|
+
*/
|
|
411
|
+
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
412
|
};
|
|
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
413
|
|
|
326
414
|
//#endregion
|
|
327
415
|
//#region src/container.d.ts
|
|
328
416
|
/**
|
|
329
|
-
*
|
|
417
|
+
* Factory function that creates a dependency instance.
|
|
330
418
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* that the service being created needs.
|
|
334
|
-
*
|
|
335
|
-
* The factory can be either synchronous (returning T directly) or asynchronous
|
|
336
|
-
* (returning Promise<T>). The container handles both cases transparently.
|
|
419
|
+
* Receives a resolution context for injecting other dependencies.
|
|
420
|
+
* Can be synchronous or asynchronous.
|
|
337
421
|
*
|
|
338
422
|
* @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
|
-
* ```
|
|
423
|
+
* @template TRequires - Union type of required dependencies
|
|
358
424
|
*/
|
|
359
425
|
type Factory<T, TRequires extends AnyTag> = (ctx: ResolutionContext<TRequires>) => PromiseOrValue<T>;
|
|
360
426
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* Finalizers are optional cleanup functions that are called when the container
|
|
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
|
-
* ```
|
|
427
|
+
* Cleanup function called when the container is destroyed.
|
|
385
428
|
*
|
|
386
|
-
* @
|
|
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
|
-
* ```
|
|
429
|
+
* @template T - The type of the service instance being cleaned up
|
|
399
430
|
*/
|
|
400
431
|
type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
401
432
|
/**
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* This interface is used when registering dependencies that need cleanup. Instead of
|
|
405
|
-
* passing separate factory and finalizer parameters, you can pass an object
|
|
406
|
-
* containing both.
|
|
433
|
+
* Complete dependency lifecycle with factory and optional cleanup.
|
|
407
434
|
*
|
|
408
|
-
*
|
|
409
|
-
* organization and reuse. This is particularly useful when you have complex
|
|
410
|
-
* lifecycle logic or want to share lifecycle definitions across multiple services.
|
|
435
|
+
* Can be implemented as a class for complex lifecycle logic.
|
|
411
436
|
*
|
|
412
437
|
* @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
|
-
* ```
|
|
438
|
+
* @template TRequires - Union type of required dependencies
|
|
490
439
|
*/
|
|
491
440
|
interface DependencyLifecycle<T, TRequires extends AnyTag> {
|
|
492
441
|
create: Factory<T, TRequires>;
|
|
493
442
|
cleanup?: Finalizer<T>;
|
|
494
443
|
}
|
|
495
444
|
/**
|
|
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
|
-
* ```
|
|
445
|
+
* Valid dependency registration: either a factory function or lifecycle object.
|
|
522
446
|
*/
|
|
523
447
|
type DependencySpec<T extends AnyTag, TRequires extends AnyTag> = Factory<TagType<T>, TRequires> | DependencyLifecycle<TagType<T>, TRequires>;
|
|
524
448
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* This type contains only the `resolve` and `resolveAll` methods from the container, which are used to retrieve
|
|
528
|
-
* other dependencies during the creation of a service.
|
|
449
|
+
* Context available to factory functions during resolution.
|
|
529
450
|
*
|
|
530
|
-
*
|
|
451
|
+
* Provides `resolve` and `resolveAll` for injecting dependencies.
|
|
531
452
|
*/
|
|
532
453
|
type ResolutionContext<TTags extends AnyTag> = Pick<IContainer<TTags>, 'resolve' | 'resolveAll'>;
|
|
454
|
+
/**
|
|
455
|
+
* Unique symbol for container type branding.
|
|
456
|
+
*/
|
|
533
457
|
declare const ContainerTypeId: unique symbol;
|
|
534
458
|
/**
|
|
535
|
-
* Interface
|
|
459
|
+
* Interface for dependency containers.
|
|
536
460
|
*
|
|
537
|
-
* @template TTags - Union type of
|
|
461
|
+
* @template TTags - Union type of registered dependency tags
|
|
538
462
|
*/
|
|
539
463
|
interface IContainer<TTags extends AnyTag = never> {
|
|
540
464
|
readonly [ContainerTypeId]: {
|
|
541
465
|
readonly _TTags: Contravariant<TTags>;
|
|
542
466
|
};
|
|
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
467
|
resolve: <T extends TTags>(tag: T) => Promise<TagType<T>>;
|
|
547
468
|
resolveAll: <const T extends readonly TTags[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
469
|
+
use: <T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>) => Promise<R>;
|
|
548
470
|
destroy(): Promise<void>;
|
|
549
471
|
}
|
|
550
472
|
/**
|
|
551
|
-
* Extracts the registered tags
|
|
473
|
+
* Extracts the registered tags from a container type.
|
|
552
474
|
*/
|
|
553
475
|
type ContainerTags<C> = C extends IContainer<infer TTags> ? TTags : never;
|
|
554
476
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
|
|
477
|
+
* Common interface for container builders that support adding dependencies.
|
|
478
|
+
* Used by Layer to work with both ContainerBuilder and ScopedContainerBuilder.
|
|
479
|
+
*/
|
|
480
|
+
interface IContainerBuilder<TTags extends AnyTag = never> {
|
|
481
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): IContainerBuilder<TTags | T>;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Extracts the registered tags from a builder type.
|
|
485
|
+
*/
|
|
486
|
+
type BuilderTags<B> = B extends IContainerBuilder<infer TTags> ? TTags : never;
|
|
487
|
+
/**
|
|
488
|
+
* Builder for constructing immutable containers.
|
|
558
489
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* and preventing runtime errors.
|
|
490
|
+
* Use `Container.builder()` to create a builder, then chain `.add()` calls
|
|
491
|
+
* to register dependencies, and finally call `.build()` to create the container.
|
|
562
492
|
*
|
|
563
|
-
* @template TTags - Union type of
|
|
493
|
+
* @template TTags - Union type of registered dependency tags
|
|
564
494
|
*
|
|
565
|
-
* @example
|
|
495
|
+
* @example
|
|
566
496
|
* ```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);
|
|
497
|
+
* const container = Container.builder()
|
|
498
|
+
* .add(Database, () => new Database())
|
|
499
|
+
* .add(UserService, async (ctx) =>
|
|
500
|
+
* new UserService(await ctx.resolve(Database))
|
|
501
|
+
* )
|
|
502
|
+
* .build();
|
|
585
503
|
* ```
|
|
504
|
+
*/
|
|
505
|
+
declare class ContainerBuilder<TTags extends AnyTag = never> {
|
|
506
|
+
private readonly factories;
|
|
507
|
+
private readonly finalizers;
|
|
508
|
+
/**
|
|
509
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
510
|
+
*
|
|
511
|
+
* @param tag - The dependency tag (class or ValueTag)
|
|
512
|
+
* @param spec - Factory function or lifecycle object
|
|
513
|
+
* @returns The builder with updated type information
|
|
514
|
+
*/
|
|
515
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ContainerBuilder<TTags | T>;
|
|
516
|
+
/**
|
|
517
|
+
* Creates an immutable container from the registered dependencies.
|
|
518
|
+
*/
|
|
519
|
+
build(): Container<TTags>;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Type-safe dependency injection container.
|
|
586
523
|
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
590
|
-
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
591
|
-
*
|
|
592
|
-
* const container = Container.empty()
|
|
593
|
-
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
594
|
-
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
524
|
+
* Containers are immutable - use `Container.builder()` to create one.
|
|
525
|
+
* Each dependency is created once (singleton) and cached.
|
|
595
526
|
*
|
|
596
|
-
*
|
|
597
|
-
* const config = await c.resolve(ConfigTag);
|
|
598
|
-
* ```
|
|
527
|
+
* @template TTags - Union type of registered dependency tags
|
|
599
528
|
*
|
|
600
|
-
* @example
|
|
529
|
+
* @example
|
|
601
530
|
* ```typescript
|
|
602
|
-
* class
|
|
603
|
-
*
|
|
604
|
-
*
|
|
531
|
+
* class Database {
|
|
532
|
+
* query(sql: string) { return []; }
|
|
533
|
+
* }
|
|
534
|
+
*
|
|
535
|
+
* class UserService {
|
|
536
|
+
* constructor(private db: Database) {}
|
|
537
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
605
538
|
* }
|
|
606
539
|
*
|
|
607
|
-
* const container = Container.
|
|
608
|
-
*
|
|
609
|
-
* async () =>
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* },
|
|
614
|
-
* async (conn) => conn.disconnect() // Finalizer for cleanup
|
|
615
|
-
* );
|
|
540
|
+
* const container = Container.builder()
|
|
541
|
+
* .add(Database, () => new Database())
|
|
542
|
+
* .add(UserService, async (ctx) =>
|
|
543
|
+
* new UserService(await ctx.resolve(Database))
|
|
544
|
+
* )
|
|
545
|
+
* .build();
|
|
616
546
|
*
|
|
617
|
-
*
|
|
618
|
-
* await c.destroy(); // Calls all finalizers
|
|
547
|
+
* const userService = await container.resolve(UserService);
|
|
619
548
|
* ```
|
|
620
549
|
*/
|
|
621
|
-
declare class Container<TTags extends AnyTag> implements IContainer<TTags> {
|
|
550
|
+
declare class Container<TTags extends AnyTag = never> implements IContainer<TTags> {
|
|
622
551
|
readonly [ContainerTypeId]: {
|
|
623
552
|
readonly _TTags: Contravariant<TTags>;
|
|
624
553
|
};
|
|
625
|
-
protected constructor();
|
|
626
554
|
/**
|
|
627
|
-
* Cache of instantiated dependencies
|
|
628
|
-
* Ensures singleton behavior and supports concurrent access.
|
|
555
|
+
* Cache of instantiated dependencies.
|
|
629
556
|
* @internal
|
|
630
557
|
*/
|
|
631
558
|
protected readonly cache: Map<AnyTag, Promise<unknown>>;
|
|
632
559
|
/**
|
|
633
|
-
* Factory functions for creating
|
|
560
|
+
* Factory functions for creating dependencies.
|
|
634
561
|
* @internal
|
|
635
562
|
*/
|
|
636
563
|
protected readonly factories: Map<AnyTag, Factory<unknown, TTags>>;
|
|
637
564
|
/**
|
|
638
|
-
*
|
|
565
|
+
* Cleanup functions for dependencies.
|
|
639
566
|
* @internal
|
|
640
567
|
*/
|
|
641
568
|
protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
|
|
642
569
|
/**
|
|
643
|
-
*
|
|
570
|
+
* Whether this container has been destroyed.
|
|
644
571
|
* @internal
|
|
645
572
|
*/
|
|
646
573
|
protected isDestroyed: boolean;
|
|
647
574
|
/**
|
|
648
|
-
*
|
|
649
|
-
* @returns A new empty Container instance with no registered dependencies.
|
|
575
|
+
* @internal - Use Container.builder() or Container.empty()
|
|
650
576
|
*/
|
|
651
|
-
|
|
577
|
+
protected constructor(factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
|
|
652
578
|
/**
|
|
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
|
-
* ```
|
|
579
|
+
* @internal - Used by ContainerBuilder
|
|
580
|
+
*/
|
|
581
|
+
static _createFromBuilder<T extends AnyTag>(factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): Container<T>;
|
|
582
|
+
/**
|
|
583
|
+
* Creates a new container builder.
|
|
681
584
|
*
|
|
682
|
-
* @example
|
|
585
|
+
* @example
|
|
683
586
|
* ```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
|
-
* );
|
|
587
|
+
* const container = Container.builder()
|
|
588
|
+
* .add(Database, () => new Database())
|
|
589
|
+
* .build();
|
|
697
590
|
* ```
|
|
591
|
+
*/
|
|
592
|
+
static builder(): ContainerBuilder;
|
|
593
|
+
/**
|
|
594
|
+
* Creates an empty container with no dependencies.
|
|
698
595
|
*
|
|
699
|
-
*
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
*
|
|
704
|
-
* ```
|
|
596
|
+
* Shorthand for `Container.builder().build()`.
|
|
597
|
+
*/
|
|
598
|
+
static empty(): Container;
|
|
599
|
+
/**
|
|
600
|
+
* Creates a scoped container for hierarchical dependency management.
|
|
705
601
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
602
|
+
* Scoped containers support parent/child relationships where children
|
|
603
|
+
* can access parent dependencies but maintain their own cache.
|
|
709
604
|
*
|
|
710
|
-
*
|
|
711
|
-
* ConfigTag,
|
|
712
|
-
* () => ({ apiUrl: 'https://api.example.com' })
|
|
713
|
-
* );
|
|
714
|
-
* ```
|
|
605
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
715
606
|
*
|
|
716
|
-
* @example
|
|
607
|
+
* @example
|
|
717
608
|
* ```typescript
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* async close() { return; }
|
|
721
|
-
* }
|
|
609
|
+
* const appContainer = Container.scoped('app');
|
|
610
|
+
* // ... add app-level dependencies
|
|
722
611
|
*
|
|
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
|
-
* );
|
|
612
|
+
* const requestContainer = appContainer.child('request');
|
|
613
|
+
* // ... add request-specific dependencies
|
|
732
614
|
* ```
|
|
733
615
|
*/
|
|
734
|
-
|
|
616
|
+
static scoped(scope: string | symbol): ScopedContainer;
|
|
735
617
|
/**
|
|
736
|
-
*
|
|
618
|
+
* Creates a container from a layer.
|
|
737
619
|
*
|
|
738
|
-
* This
|
|
739
|
-
*
|
|
620
|
+
* This is a convenience method equivalent to applying a layer to
|
|
621
|
+
* `Container.builder()` and building the result.
|
|
740
622
|
*
|
|
741
|
-
* @param
|
|
742
|
-
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
623
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
743
624
|
*
|
|
744
625
|
* @example
|
|
745
626
|
* ```typescript
|
|
746
|
-
* const
|
|
747
|
-
*
|
|
748
|
-
* ```
|
|
749
|
-
*/
|
|
750
|
-
has(tag: AnyTag): boolean;
|
|
751
|
-
/**
|
|
752
|
-
* Checks if a dependency has been instantiated (cached) in the container.
|
|
627
|
+
* const dbLayer = Layer.service(Database, []);
|
|
628
|
+
* const container = Container.from(dbLayer);
|
|
753
629
|
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
630
|
+
* const db = await container.resolve(Database);
|
|
631
|
+
* ```
|
|
756
632
|
*/
|
|
757
|
-
|
|
633
|
+
static from<TProvides extends AnyTag>(layer: Layer<never, TProvides>): Container<TProvides>;
|
|
758
634
|
/**
|
|
759
|
-
*
|
|
635
|
+
* Resolves a dependency, creating it if necessary.
|
|
760
636
|
*
|
|
761
|
-
*
|
|
762
|
-
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
763
|
-
* requests for the same dependency correctly.
|
|
637
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
764
638
|
*
|
|
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
|
|
639
|
+
* @param tag - The dependency tag to resolve
|
|
640
|
+
* @returns Promise resolving to the dependency instance
|
|
641
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
642
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
772
643
|
* @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
|
-
* ```
|
|
644
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
807
645
|
*/
|
|
808
646
|
resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
|
|
809
647
|
/**
|
|
810
|
-
* Internal resolution
|
|
811
|
-
* Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
|
|
648
|
+
* Internal resolution with dependency chain tracking.
|
|
812
649
|
* @internal
|
|
813
650
|
*/
|
|
814
651
|
protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
815
652
|
/**
|
|
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.
|
|
653
|
+
* Resolves multiple dependencies concurrently.
|
|
822
654
|
*
|
|
823
|
-
* @
|
|
824
|
-
* @
|
|
825
|
-
* @returns Promise resolving to a tuple of service instances in the same order
|
|
655
|
+
* @param tags - The dependency tags to resolve
|
|
656
|
+
* @returns Promise resolving to a tuple of instances
|
|
826
657
|
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
827
658
|
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
828
659
|
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
829
660
|
* @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
661
|
*/
|
|
855
662
|
resolveAll<const T extends readonly TTags[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
856
663
|
/**
|
|
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.
|
|
664
|
+
* Resolves a service, runs the callback with it, then destroys the container.
|
|
867
665
|
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* dependencies are cleaned up.
|
|
666
|
+
* This is a convenience method for the common "create, use, destroy" pattern.
|
|
667
|
+
* The container is always destroyed after the callback completes, even if it throws.
|
|
871
668
|
*
|
|
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
|
-
* ```
|
|
669
|
+
* @param tag - The dependency tag to resolve
|
|
670
|
+
* @param fn - Callback that receives the resolved service
|
|
671
|
+
* @returns Promise resolving to the callback's return value
|
|
672
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
673
|
+
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
674
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
675
|
+
* @throws {DependencyCreationError} If the factory function throws
|
|
676
|
+
* @throws {DependencyFinalizationError} If the finalizer function throws
|
|
914
677
|
*
|
|
915
|
-
* @example
|
|
678
|
+
* @example
|
|
916
679
|
* ```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
|
|
680
|
+
* const result = await container.use(UserService, (service) =>
|
|
681
|
+
* service.getUsers()
|
|
682
|
+
* );
|
|
683
|
+
* // Container is automatically destroyed after callback completes
|
|
925
684
|
* ```
|
|
926
685
|
*/
|
|
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>>;
|
|
686
|
+
use<T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>): Promise<R>;
|
|
980
687
|
/**
|
|
981
|
-
* Destroys
|
|
688
|
+
* Destroys the container, calling all finalizers.
|
|
982
689
|
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
690
|
+
* After destruction, the container cannot be used.
|
|
691
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
692
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
693
|
+
* dependencies are cleaned up.
|
|
987
694
|
*
|
|
988
|
-
*
|
|
989
|
-
* before their dependents.
|
|
695
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
990
696
|
*/
|
|
991
697
|
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
698
|
}
|
|
1000
|
-
|
|
1001
|
-
//#endregion
|
|
1002
|
-
//#region src/layer.d.ts
|
|
1003
699
|
/**
|
|
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.
|
|
700
|
+
* Scope identifier type.
|
|
1016
701
|
*/
|
|
1017
|
-
type
|
|
1018
|
-
/**
|
|
1019
|
-
* The type ID for the Layer interface.
|
|
1020
|
-
*/
|
|
1021
|
-
declare const LayerTypeId: unique symbol;
|
|
702
|
+
type Scope = string | symbol;
|
|
1022
703
|
/**
|
|
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
|
-
* );
|
|
704
|
+
* Builder for constructing scoped containers.
|
|
1072
705
|
*
|
|
1073
|
-
*
|
|
1074
|
-
* const appLayer = userLayer.provide(databaseLayer);
|
|
1075
|
-
* ```
|
|
706
|
+
* @template TTags - Union type of registered dependency tags
|
|
1076
707
|
*/
|
|
1077
|
-
|
|
1078
|
-
readonly
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
708
|
+
declare class ScopedContainerBuilder<TTags extends AnyTag = never> {
|
|
709
|
+
private readonly scope;
|
|
710
|
+
private readonly parent;
|
|
711
|
+
private readonly factories;
|
|
712
|
+
private readonly finalizers;
|
|
713
|
+
constructor(scope: Scope, parent: IContainer<TTags> | null);
|
|
1082
714
|
/**
|
|
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
|
-
* ```
|
|
715
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
1111
716
|
*/
|
|
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
|
-
* ```
|
|
717
|
+
add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainerBuilder<TTags | T>;
|
|
718
|
+
/**
|
|
719
|
+
* Creates an immutable scoped container from the registered dependencies.
|
|
1203
720
|
*/
|
|
1204
|
-
|
|
721
|
+
build(): ScopedContainer<TTags>;
|
|
1205
722
|
}
|
|
1206
723
|
/**
|
|
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
|
-
* }
|
|
724
|
+
* Scoped container for hierarchical dependency management.
|
|
1224
725
|
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
* );
|
|
726
|
+
* Supports parent/child relationships where children can access parent
|
|
727
|
+
* dependencies but maintain their own cache. Useful for request-scoped
|
|
728
|
+
* dependencies in web applications.
|
|
1229
729
|
*
|
|
1230
|
-
*
|
|
1231
|
-
* const dbLayerInstance = databaseLayer;
|
|
1232
|
-
* ```
|
|
730
|
+
* @template TTags - Union type of registered dependency tags
|
|
1233
731
|
*
|
|
1234
|
-
* @example
|
|
732
|
+
* @example
|
|
1235
733
|
* ```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);
|
|
734
|
+
* // Application-level container
|
|
735
|
+
* const appContainer = ScopedContainer.builder('app')
|
|
736
|
+
* .add(Database, () => new Database())
|
|
737
|
+
* .build();
|
|
738
|
+
*
|
|
739
|
+
* // Request-level container (inherits from app)
|
|
740
|
+
* const requestContainer = appContainer.child('request')
|
|
741
|
+
* .add(RequestContext, () => new RequestContext())
|
|
742
|
+
* .build();
|
|
743
|
+
*
|
|
744
|
+
* // Can resolve both app and request dependencies
|
|
745
|
+
* const db = await requestContainer.resolve(Database);
|
|
746
|
+
* const ctx = await requestContainer.resolve(RequestContext);
|
|
1259
747
|
* ```
|
|
1260
748
|
*/
|
|
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: {
|
|
749
|
+
declare class ScopedContainer<TTags extends AnyTag = never> extends Container<TTags> {
|
|
750
|
+
readonly scope: Scope;
|
|
751
|
+
private parent;
|
|
752
|
+
private readonly children;
|
|
753
|
+
/**
|
|
754
|
+
* @internal
|
|
755
|
+
*/
|
|
756
|
+
protected constructor(scope: Scope, parent: IContainer<TTags> | null, factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
|
|
1288
757
|
/**
|
|
1289
|
-
*
|
|
1290
|
-
|
|
758
|
+
* @internal - Used by ScopedContainerBuilder
|
|
759
|
+
*/
|
|
760
|
+
static _createScopedFromBuilder<T extends AnyTag>(scope: Scope, parent: IContainer<T> | null, factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): ScopedContainer<T>;
|
|
761
|
+
/**
|
|
762
|
+
* Creates a new scoped container builder.
|
|
1291
763
|
*
|
|
1292
|
-
* @
|
|
764
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
1293
765
|
*
|
|
1294
766
|
* @example
|
|
1295
767
|
* ```typescript
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
* const appLayer = baseLayer
|
|
1300
|
-
* .merge(configLayer)
|
|
1301
|
-
* .merge(serviceLayer);
|
|
768
|
+
* const container = ScopedContainer.builder('app')
|
|
769
|
+
* .add(Database, () => new Database())
|
|
770
|
+
* .build();
|
|
1302
771
|
* ```
|
|
1303
772
|
*/
|
|
1304
|
-
|
|
773
|
+
static builder(scope: Scope): ScopedContainerBuilder;
|
|
1305
774
|
/**
|
|
1306
|
-
*
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
775
|
+
* Creates an empty scoped container with no dependencies.
|
|
776
|
+
*/
|
|
777
|
+
static empty(scope: Scope): ScopedContainer;
|
|
778
|
+
/**
|
|
779
|
+
* Creates a scoped container from a layer.
|
|
1310
780
|
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
781
|
+
* This is a convenience method equivalent to applying a layer to
|
|
782
|
+
* `ScopedContainer.builder()` and building the result.
|
|
1313
783
|
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* - **Covariant TProvides**: Layer<Y, typeof ServiceB> can be passed because providing
|
|
1317
|
-
* ServiceB is compatible with the general `AnyTag` type
|
|
784
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
785
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
1318
786
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
787
|
+
* @example
|
|
788
|
+
* ```typescript
|
|
789
|
+
* const dbLayer = Layer.service(Database, []);
|
|
790
|
+
* const container = ScopedContainer.from('app', dbLayer);
|
|
1321
791
|
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
|
|
792
|
+
* const db = await container.resolve(Database);
|
|
793
|
+
* ```
|
|
794
|
+
*/
|
|
795
|
+
static from<TProvides extends AnyTag>(scope: Scope, layer: Layer<never, TProvides>): ScopedContainer<TProvides>;
|
|
796
|
+
/**
|
|
797
|
+
* Resolves a dependency from this scope or parent scopes, creating it if necessary.
|
|
1325
798
|
*
|
|
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
|
|
799
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
1329
800
|
*
|
|
1330
|
-
* @
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
801
|
+
* @param tag - The dependency tag to resolve
|
|
802
|
+
* @returns Promise resolving to the dependency instance
|
|
803
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
804
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
805
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
806
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
807
|
+
*/
|
|
808
|
+
resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
|
|
809
|
+
/**
|
|
810
|
+
* Internal resolution with parent delegation.
|
|
811
|
+
* @internal
|
|
812
|
+
*/
|
|
813
|
+
protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
|
|
814
|
+
/**
|
|
815
|
+
* @internal - Used by ScopedContainerBuilder to register children
|
|
816
|
+
*/
|
|
817
|
+
_registerChild(child: ScopedContainer<TTags>): void;
|
|
818
|
+
/**
|
|
819
|
+
* Creates a child container builder that inherits from this container.
|
|
1333
820
|
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
* const userLayer = layer<typeof DatabaseService, typeof UserService>(...); // requires DB
|
|
1337
|
-
* const configLayer = layer<never, typeof ConfigService>(...); // no requirements
|
|
821
|
+
* Use this to create a child scope and add dependencies to it.
|
|
822
|
+
* The child can resolve dependencies from this container.
|
|
1338
823
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
824
|
+
* @param scope - Identifier for the child scope
|
|
825
|
+
* @returns A new ScopedContainerBuilder for the child scope
|
|
826
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
1342
827
|
*
|
|
1343
|
-
* @example
|
|
828
|
+
* @example
|
|
1344
829
|
* ```typescript
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
830
|
+
* const requestContainer = appContainer.child('request')
|
|
831
|
+
* .add(RequestContext, () => new RequestContext())
|
|
832
|
+
* .build();
|
|
833
|
+
*
|
|
834
|
+
* await requestContainer.resolve(Database); // From parent
|
|
835
|
+
* await requestContainer.resolve(RequestContext); // From this scope
|
|
1348
836
|
* ```
|
|
837
|
+
*/
|
|
838
|
+
child(scope: Scope): ScopedContainerBuilder<TTags>;
|
|
839
|
+
/**
|
|
840
|
+
* Creates a child container with a layer applied.
|
|
1349
841
|
*
|
|
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>(...);
|
|
842
|
+
* This is a convenience method combining child() + layer.apply() + build().
|
|
843
|
+
* Use this when you have a layer ready to apply.
|
|
1355
844
|
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
845
|
+
* @param scope - Identifier for the child scope
|
|
846
|
+
* @param layer - Layer to apply to the child (can require parent's tags)
|
|
847
|
+
*
|
|
848
|
+
* @example
|
|
849
|
+
* ```typescript
|
|
850
|
+
* const requestContainer = appContainer.childFrom('request',
|
|
851
|
+
* userService
|
|
852
|
+
* .provide(Layer.value(TenantContext, tenantCtx))
|
|
853
|
+
* .provide(Layer.value(RequestId, requestId))
|
|
1361
854
|
* );
|
|
1362
855
|
*
|
|
1363
|
-
*
|
|
856
|
+
* const users = await requestContainer.resolve(UserService);
|
|
1364
857
|
* ```
|
|
1365
858
|
*/
|
|
1366
|
-
|
|
859
|
+
childFrom<TProvides extends AnyTag>(scope: Scope, layer: Layer<TTags, TProvides>): ScopedContainer<TTags | TProvides>;
|
|
1367
860
|
/**
|
|
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';
|
|
861
|
+
* Destroys this container and all child containers.
|
|
1382
862
|
*
|
|
1383
|
-
*
|
|
1384
|
-
* const cacheLayer = layer<never, typeof CacheService>(...);
|
|
863
|
+
* Children are destroyed first to ensure proper cleanup order.
|
|
1385
864
|
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
865
|
+
* After destruction, the container cannot be used.
|
|
866
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
867
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
868
|
+
* dependencies are cleaned up.
|
|
869
|
+
*
|
|
870
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
1389
871
|
*/
|
|
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>;
|
|
872
|
+
destroy(): Promise<void>;
|
|
873
|
+
}
|
|
1414
874
|
|
|
1415
875
|
//#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];
|
|
876
|
+
//#region src/errors.d.ts
|
|
1423
877
|
/**
|
|
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
|
-
* ```
|
|
878
|
+
* Structured error information for debugging and logging.
|
|
1503
879
|
*/
|
|
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
880
|
type ErrorDump = {
|
|
1509
881
|
name: string;
|
|
1510
882
|
message: string;
|
|
@@ -1512,17 +884,20 @@ type ErrorDump = {
|
|
|
1512
884
|
detail: Record<string, unknown>;
|
|
1513
885
|
cause?: unknown;
|
|
1514
886
|
};
|
|
887
|
+
/**
|
|
888
|
+
* Options for creating Sandly errors.
|
|
889
|
+
*/
|
|
1515
890
|
type SandlyErrorOptions = {
|
|
1516
891
|
cause?: unknown;
|
|
1517
892
|
detail?: Record<string, unknown>;
|
|
1518
893
|
};
|
|
1519
894
|
/**
|
|
1520
|
-
* Base error class for all library errors.
|
|
895
|
+
* Base error class for all Sandly library errors.
|
|
1521
896
|
*
|
|
1522
|
-
*
|
|
897
|
+
* Extends the native Error class to provide consistent error handling
|
|
1523
898
|
* and structured error information across the library.
|
|
1524
899
|
*
|
|
1525
|
-
* @example
|
|
900
|
+
* @example
|
|
1526
901
|
* ```typescript
|
|
1527
902
|
* try {
|
|
1528
903
|
* await container.resolve(SomeService);
|
|
@@ -1540,44 +915,36 @@ declare class SandlyError extends Error {
|
|
|
1540
915
|
cause,
|
|
1541
916
|
detail
|
|
1542
917
|
}?: SandlyErrorOptions);
|
|
918
|
+
/**
|
|
919
|
+
* Wraps any error as a SandlyError.
|
|
920
|
+
*/
|
|
1543
921
|
static ensure(error: unknown): SandlyError;
|
|
922
|
+
/**
|
|
923
|
+
* Returns a structured representation of the error for logging.
|
|
924
|
+
*/
|
|
1544
925
|
dump(): ErrorDump;
|
|
926
|
+
/**
|
|
927
|
+
* Returns a JSON string representation of the error.
|
|
928
|
+
*/
|
|
1545
929
|
dumps(): string;
|
|
1546
930
|
/**
|
|
1547
931
|
* Recursively extract cause chain from any Error.
|
|
1548
|
-
* Handles both AppError (with dump()) and plain Errors (with cause property).
|
|
1549
932
|
*/
|
|
1550
933
|
private dumpCause;
|
|
1551
934
|
}
|
|
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
935
|
/**
|
|
1561
936
|
* 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
937
|
*/
|
|
1567
938
|
declare class ContainerDestroyedError extends SandlyError {}
|
|
1568
939
|
/**
|
|
1569
940
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
1570
941
|
*
|
|
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
942
|
* @example
|
|
1576
943
|
* ```typescript
|
|
1577
|
-
* const container = Container.
|
|
944
|
+
* const container = Container.builder().build(); // Empty container
|
|
1578
945
|
*
|
|
1579
946
|
* try {
|
|
1580
|
-
* await
|
|
947
|
+
* await container.resolve(UnregisteredService);
|
|
1581
948
|
* } catch (error) {
|
|
1582
949
|
* if (error instanceof UnknownDependencyError) {
|
|
1583
950
|
* console.error('Missing dependency:', error.message);
|
|
@@ -1586,360 +953,81 @@ declare class ContainerDestroyedError extends SandlyError {}
|
|
|
1586
953
|
* ```
|
|
1587
954
|
*/
|
|
1588
955
|
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
956
|
constructor(tag: AnyTag);
|
|
1596
957
|
}
|
|
1597
958
|
/**
|
|
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.
|
|
959
|
+
* Error thrown when a circular dependency is detected during resolution.
|
|
1603
960
|
*
|
|
1604
|
-
* @example
|
|
961
|
+
* @example
|
|
1605
962
|
* ```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
|
-
*
|
|
963
|
+
* // ServiceA depends on ServiceB, ServiceB depends on ServiceA
|
|
1617
964
|
* try {
|
|
1618
|
-
* await
|
|
965
|
+
* await container.resolve(ServiceA);
|
|
1619
966
|
* } catch (error) {
|
|
1620
967
|
* if (error instanceof CircularDependencyError) {
|
|
1621
968
|
* console.error('Circular dependency:', error.message);
|
|
1622
|
-
* //
|
|
969
|
+
* // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
1623
970
|
* }
|
|
1624
971
|
* }
|
|
1625
972
|
* ```
|
|
1626
973
|
*/
|
|
1627
974
|
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
975
|
constructor(tag: AnyTag, dependencyChain: AnyTag[]);
|
|
1636
976
|
}
|
|
1637
977
|
/**
|
|
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.
|
|
978
|
+
* Error thrown when a dependency factory throws during instantiation.
|
|
1642
979
|
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
980
|
+
* For nested dependencies (A depends on B depends on C), use `getRootCause()`
|
|
981
|
+
* to unwrap all layers and get the original error.
|
|
1645
982
|
*
|
|
1646
|
-
* @example
|
|
983
|
+
* @example
|
|
1647
984
|
* ```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
985
|
* try {
|
|
1655
|
-
* await
|
|
986
|
+
* await container.resolve(UserService);
|
|
1656
987
|
* } catch (error) {
|
|
1657
988
|
* if (error instanceof DependencyCreationError) {
|
|
1658
989
|
* 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
990
|
* const rootCause = error.getRootCause();
|
|
1673
|
-
* console.error('Root cause:', rootCause);
|
|
991
|
+
* console.error('Root cause:', rootCause);
|
|
1674
992
|
* }
|
|
1675
993
|
* }
|
|
1676
994
|
* ```
|
|
1677
995
|
*/
|
|
1678
996
|
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
997
|
constructor(tag: AnyTag, error: unknown);
|
|
1687
998
|
/**
|
|
1688
999
|
* Traverses the error chain to find the root cause error.
|
|
1689
1000
|
*
|
|
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
|
-
* ```
|
|
1001
|
+
* When dependencies are nested, each level wraps the error.
|
|
1002
|
+
* This method unwraps all layers to get the original error.
|
|
1706
1003
|
*/
|
|
1707
1004
|
getRootCause(): unknown;
|
|
1708
1005
|
}
|
|
1709
1006
|
/**
|
|
1710
1007
|
* Error thrown when one or more finalizers fail during container destruction.
|
|
1711
1008
|
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
* process continues and this error contains details of all failures.
|
|
1009
|
+
* Even if some finalizers fail, cleanup continues for all others.
|
|
1010
|
+
* This error aggregates all failures.
|
|
1715
1011
|
*
|
|
1716
|
-
* @example
|
|
1012
|
+
* @example
|
|
1717
1013
|
* ```typescript
|
|
1718
1014
|
* try {
|
|
1719
1015
|
* await container.destroy();
|
|
1720
1016
|
* } catch (error) {
|
|
1721
1017
|
* if (error instanceof DependencyFinalizationError) {
|
|
1722
|
-
* console.error('
|
|
1723
|
-
* console.error('Error details:', error.detail.errors);
|
|
1018
|
+
* console.error('Cleanup failures:', error.getRootCauses());
|
|
1724
1019
|
* }
|
|
1725
1020
|
* }
|
|
1726
1021
|
* ```
|
|
1727
1022
|
*/
|
|
1728
1023
|
declare class DependencyFinalizationError extends SandlyError {
|
|
1729
1024
|
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
1025
|
constructor(errors: unknown[]);
|
|
1737
1026
|
/**
|
|
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.
|
|
1027
|
+
* Returns all root cause errors from the finalization failures.
|
|
1742
1028
|
*/
|
|
1743
1029
|
getRootCauses(): unknown[];
|
|
1744
1030
|
}
|
|
1745
1031
|
|
|
1746
1032
|
//#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 };
|
|
1033
|
+
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 };
|