sandly 0.0.2 → 0.2.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 +19 -16
- package/dist/index.d.ts +1029 -470
- package/dist/index.js +572 -417
- package/package.json +75 -76
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
//#region src/tag.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* This symbol is used as a property key to attach metadata to both value tags and class tags.
|
|
3
|
+
* Type representing a tag identifier (string or symbol).
|
|
5
4
|
* @internal
|
|
6
5
|
*/
|
|
7
|
-
|
|
6
|
+
type TagId = string | symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Symbol used to identify tagged types within the dependency injection system.
|
|
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
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
declare const ValueTagIdKey = "__sandly/ValueTagIdKey__";
|
|
17
|
+
declare const ServiceTagIdKey = "__sandly/ServiceTagIdKey__";
|
|
18
|
+
/**
|
|
19
|
+
* Internal symbol used to identify the type of a tagged type within the dependency injection system.
|
|
20
|
+
* This symbol 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.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
declare const TagTypeKey: unique symbol;
|
|
8
25
|
/**
|
|
9
26
|
* Type representing a value-based dependency tag.
|
|
10
27
|
*
|
|
@@ -18,31 +35,30 @@ declare const TagId: "__tag_id__";
|
|
|
18
35
|
* @example
|
|
19
36
|
* ```typescript
|
|
20
37
|
* // Creates a value tag for string configuration
|
|
21
|
-
* const ApiKeyTag: ValueTag<
|
|
38
|
+
* const ApiKeyTag: ValueTag<'apiKey', string> = Tag.of('apiKey')<string>();
|
|
22
39
|
*
|
|
23
40
|
* // Register in container
|
|
24
41
|
* container.register(ApiKeyTag, () => 'my-secret-key');
|
|
25
42
|
* ```
|
|
26
43
|
*/
|
|
27
|
-
|
|
28
|
-
readonly [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}>;
|
|
44
|
+
interface ValueTag<Id extends TagId, T> {
|
|
45
|
+
readonly [ValueTagIdKey]: Id;
|
|
46
|
+
readonly [TagTypeKey]: T;
|
|
47
|
+
}
|
|
32
48
|
/**
|
|
33
49
|
* Type representing a class-based dependency tag.
|
|
34
50
|
*
|
|
35
|
-
* Tagged classes are created by Tag.
|
|
51
|
+
* Tagged classes are created by Tag.Service() and serve as both the dependency identifier
|
|
36
52
|
* and the constructor for the service. They extend regular classes with tag metadata
|
|
37
53
|
* that the DI system uses for identification and type safety.
|
|
38
54
|
*
|
|
39
|
-
* @template T - The type of instances created by this tagged class
|
|
40
55
|
* @template Id - The unique identifier for this tag (string or symbol)
|
|
56
|
+
* @template T - The type of instances created by this tagged class
|
|
41
57
|
*
|
|
42
58
|
* @example
|
|
43
59
|
* ```typescript
|
|
44
60
|
* // Creates a tagged class
|
|
45
|
-
* class UserService extends Tag.
|
|
61
|
+
* class UserService extends Tag.Service('UserService') {
|
|
46
62
|
* getUsers() { return []; }
|
|
47
63
|
* }
|
|
48
64
|
*
|
|
@@ -50,25 +66,53 @@ type ValueTag<T, Id extends string | symbol> = Readonly<{
|
|
|
50
66
|
* container.register(UserService, () => new UserService());
|
|
51
67
|
* ```
|
|
52
68
|
*
|
|
53
|
-
* @internal - Users should use Tag.
|
|
69
|
+
* @internal - Users should use Tag.Service() instead of working with this type directly
|
|
54
70
|
*/
|
|
55
|
-
|
|
71
|
+
interface ServiceTag<Id extends TagId, T> {
|
|
56
72
|
new (...args: any[]): T & {
|
|
57
|
-
readonly [
|
|
73
|
+
readonly [ServiceTagIdKey]: Id;
|
|
58
74
|
};
|
|
59
|
-
readonly [
|
|
60
|
-
}
|
|
75
|
+
readonly [ServiceTagIdKey]: Id;
|
|
76
|
+
}
|
|
61
77
|
/**
|
|
62
|
-
*
|
|
78
|
+
* Utility type that extracts the service type from any dependency tag.
|
|
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.
|
|
63
83
|
*
|
|
64
|
-
*
|
|
84
|
+
* @template T - Any dependency tag (ValueTag or ServiceTag)
|
|
85
|
+
* @returns The service type that the tag represents
|
|
65
86
|
*
|
|
66
|
-
* @
|
|
67
|
-
*
|
|
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
|
|
68
110
|
*
|
|
69
|
-
*
|
|
111
|
+
* const str: string = await container.resolve(StringTag); // Automatically typed as string
|
|
112
|
+
* const user: UserService = await container.resolve(UserService); // Automatically typed as UserService
|
|
113
|
+
* ```
|
|
70
114
|
*/
|
|
71
|
-
type
|
|
115
|
+
type TagType<TTag extends AnyTag> = TTag extends ValueTag<any, infer T> ? T : TTag extends ServiceTag<any, infer T> ? T : never;
|
|
72
116
|
/**
|
|
73
117
|
* Union type representing any valid dependency tag in the system.
|
|
74
118
|
*
|
|
@@ -84,15 +128,15 @@ type ClassTag<T> = TaggedClass<T, string | symbol>;
|
|
|
84
128
|
*
|
|
85
129
|
* @example Class tag
|
|
86
130
|
* ```typescript
|
|
87
|
-
* class DatabaseService extends Tag.
|
|
131
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
88
132
|
* // DatabaseService satisfies AnyTag
|
|
89
133
|
* ```
|
|
90
134
|
*/
|
|
91
|
-
type AnyTag = ValueTag<
|
|
135
|
+
type AnyTag = ValueTag<TagId, any> | ServiceTag<TagId, any>;
|
|
92
136
|
/**
|
|
93
137
|
* Utility object containing factory functions for creating dependency tags.
|
|
94
138
|
*
|
|
95
|
-
* The Tag object provides the primary API for creating both value tags and
|
|
139
|
+
* The Tag object provides the primary API for creating both value tags and service tags
|
|
96
140
|
* used throughout the dependency injection system. It's the main entry point for
|
|
97
141
|
* defining dependencies in a type-safe way.
|
|
98
142
|
*/
|
|
@@ -152,7 +196,7 @@ declare const Tag: {
|
|
|
152
196
|
* }));
|
|
153
197
|
* ```
|
|
154
198
|
*/
|
|
155
|
-
of: <Id extends
|
|
199
|
+
of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
|
|
156
200
|
/**
|
|
157
201
|
* Creates an anonymous value tag with a unique symbol identifier.
|
|
158
202
|
*
|
|
@@ -186,7 +230,7 @@ declare const Tag: {
|
|
|
186
230
|
* console.log(ConfigA === ConfigB); // false
|
|
187
231
|
* ```
|
|
188
232
|
*/
|
|
189
|
-
for: <T>() => ValueTag<
|
|
233
|
+
for: <T>() => ValueTag<symbol, T>;
|
|
190
234
|
/**
|
|
191
235
|
* Creates a base class that can be extended to create service classes with dependency tags.
|
|
192
236
|
*
|
|
@@ -200,7 +244,7 @@ declare const Tag: {
|
|
|
200
244
|
*
|
|
201
245
|
* @example Basic service class
|
|
202
246
|
* ```typescript
|
|
203
|
-
* class UserService extends Tag.
|
|
247
|
+
* class UserService extends Tag.Service('UserService') {
|
|
204
248
|
* getUsers() {
|
|
205
249
|
* return ['alice', 'bob'];
|
|
206
250
|
* }
|
|
@@ -211,11 +255,11 @@ declare const Tag: {
|
|
|
211
255
|
*
|
|
212
256
|
* @example Service with dependencies
|
|
213
257
|
* ```typescript
|
|
214
|
-
* class DatabaseService extends Tag.
|
|
258
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
215
259
|
* query(sql: string) { return []; }
|
|
216
260
|
* }
|
|
217
261
|
*
|
|
218
|
-
* class UserRepository extends Tag.
|
|
262
|
+
* class UserRepository extends Tag.Service('UserRepository') {
|
|
219
263
|
* constructor(private db: DatabaseService) {
|
|
220
264
|
* super();
|
|
221
265
|
* }
|
|
@@ -227,25 +271,23 @@ declare const Tag: {
|
|
|
227
271
|
*
|
|
228
272
|
* container
|
|
229
273
|
* .register(DatabaseService, () => new DatabaseService())
|
|
230
|
-
* .register(UserRepository, async (
|
|
231
|
-
* new UserRepository(await
|
|
274
|
+
* .register(UserRepository, async (ctx) =>
|
|
275
|
+
* new UserRepository(await ctx.resolve(DatabaseService))
|
|
232
276
|
* );
|
|
233
277
|
* ```
|
|
234
278
|
*
|
|
235
279
|
* @example With symbol identifiers
|
|
236
280
|
* ```typescript
|
|
237
|
-
* const
|
|
281
|
+
* const ServiceId = Symbol('InternalService');
|
|
238
282
|
*
|
|
239
|
-
* class InternalService extends Tag.
|
|
283
|
+
* class InternalService extends Tag.Service(ServiceId) {
|
|
240
284
|
* doInternalWork() { return 'work'; }
|
|
241
285
|
* }
|
|
242
286
|
* ```
|
|
243
287
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
readonly __tag_id__: Id;
|
|
248
|
-
}, Id>;
|
|
288
|
+
Service: <Id extends TagId>(id: Id) => ServiceTag<Id, {
|
|
289
|
+
readonly "__sandly/ServiceTagIdKey__": Id;
|
|
290
|
+
}>;
|
|
249
291
|
/**
|
|
250
292
|
* Extracts the string representation of a tag's identifier.
|
|
251
293
|
*
|
|
@@ -253,14 +295,14 @@ declare const Tag: {
|
|
|
253
295
|
* whether it's a string-based or symbol-based tag. Primarily used internally
|
|
254
296
|
* for error messages and debugging.
|
|
255
297
|
*
|
|
256
|
-
* @param tag - Any valid dependency tag (value tag or
|
|
298
|
+
* @param tag - Any valid dependency tag (value tag or service tag)
|
|
257
299
|
* @returns String representation of the tag's identifier
|
|
258
300
|
*
|
|
259
301
|
* @example
|
|
260
302
|
* ```typescript
|
|
261
303
|
* const StringTag = Tag.of('myString')<string>();
|
|
262
304
|
* const SymbolTag = Tag.for<number>();
|
|
263
|
-
* class ServiceClass extends Tag.
|
|
305
|
+
* class ServiceClass extends Tag.Service('MyService') {}
|
|
264
306
|
*
|
|
265
307
|
* console.log(Tag.id(StringTag)); // "myString"
|
|
266
308
|
* console.log(Tag.id(SymbolTag)); // "Symbol()"
|
|
@@ -269,65 +311,56 @@ declare const Tag: {
|
|
|
269
311
|
*
|
|
270
312
|
* @internal - Primarily for internal use in error messages and debugging
|
|
271
313
|
*/
|
|
272
|
-
id: (tag: AnyTag) =>
|
|
314
|
+
id: (tag: AnyTag) => TagId;
|
|
315
|
+
isTag: (tag: unknown) => tag is AnyTag;
|
|
273
316
|
};
|
|
274
317
|
/**
|
|
275
|
-
*
|
|
318
|
+
* Unique symbol used to store the original ValueTag in Inject<T> types.
|
|
319
|
+
* This prevents property name collisions while allowing type-level extraction.
|
|
320
|
+
*/
|
|
321
|
+
declare const InjectSource: unique symbol;
|
|
322
|
+
/**
|
|
323
|
+
* Helper type for injecting ValueTag dependencies in constructor parameters.
|
|
324
|
+
* This allows clean specification of ValueTag dependencies while preserving
|
|
325
|
+
* the original tag information for dependency inference.
|
|
276
326
|
*
|
|
277
|
-
*
|
|
278
|
-
* the container and layers to automatically determine what type of service
|
|
279
|
-
* a given tag represents without manual type annotations.
|
|
327
|
+
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
280
328
|
*
|
|
281
|
-
* @template T -
|
|
282
|
-
* @returns The
|
|
329
|
+
* @template T - A ValueTag type
|
|
330
|
+
* @returns The value type with optional phantom tag metadata for dependency inference
|
|
283
331
|
*
|
|
284
|
-
* @example
|
|
332
|
+
* @example
|
|
285
333
|
* ```typescript
|
|
286
|
-
* const
|
|
287
|
-
* const ConfigTag = Tag.of('config')<{ apiKey: string }>();
|
|
288
|
-
*
|
|
289
|
-
* type StringService = ServiceOf<typeof StringTag>; // string
|
|
290
|
-
* type ConfigService = ServiceOf<typeof ConfigTag>; // { apiKey: string }
|
|
291
|
-
* ```
|
|
334
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
292
335
|
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
336
|
+
* class UserService extends Tag.Service('UserService') {
|
|
337
|
+
* constructor(
|
|
338
|
+
* private db: DatabaseService, // ServiceTag - works automatically
|
|
339
|
+
* private apiKey: Inject<typeof ApiKeyTag> // ValueTag - type is string, tag preserved
|
|
340
|
+
* ) {
|
|
341
|
+
* super();
|
|
342
|
+
* }
|
|
297
343
|
* }
|
|
298
|
-
*
|
|
299
|
-
* type UserServiceType = ServiceOf<typeof UserService>; // UserService
|
|
300
|
-
* ```
|
|
301
|
-
*
|
|
302
|
-
* @example Used in container methods
|
|
303
|
-
* ```typescript
|
|
304
|
-
* // The container uses ServiceOf internally for type inference
|
|
305
|
-
* container.register(StringTag, () => 'hello'); // Factory must return string
|
|
306
|
-
* container.register(UserService, () => new UserService()); // Factory must return UserService
|
|
307
|
-
*
|
|
308
|
-
* const str: string = await container.get(StringTag); // Automatically typed as string
|
|
309
|
-
* const user: UserService = await container.get(UserService); // Automatically typed as UserService
|
|
310
344
|
* ```
|
|
311
345
|
*/
|
|
312
|
-
type
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
type PromiseOrValue<T> = T | Promise<T>;
|
|
316
|
-
/**
|
|
317
|
-
* Unique symbol used to store the original ValueTag in Inject<T> types.
|
|
318
|
-
* This prevents property name collisions while allowing type-level extraction.
|
|
319
|
-
*/
|
|
320
|
-
declare const InjectSource: unique symbol;
|
|
346
|
+
type Inject<T extends ValueTag<TagId, unknown>> = T extends ValueTag<any, infer V> ? V & {
|
|
347
|
+
readonly [InjectSource]?: T;
|
|
348
|
+
} : never;
|
|
321
349
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* This is primarily used internally for type constraints and validations.
|
|
325
|
-
* Most users should use Tag.Class() instead of working with raw constructors.
|
|
326
|
-
*
|
|
327
|
-
* @template T - The type that the constructor creates
|
|
350
|
+
* Helper type to extract the original ValueTag from an Inject<T> type.
|
|
351
|
+
* Since InjectSource is optional, we need to check for both presence and absence.
|
|
328
352
|
* @internal
|
|
329
353
|
*/
|
|
330
|
-
|
|
354
|
+
type ExtractInjectTag<T> = T extends {
|
|
355
|
+
readonly [InjectSource]?: infer U;
|
|
356
|
+
} ? U : never;
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/types.d.ts
|
|
359
|
+
type PromiseOrValue<T> = T | Promise<T>;
|
|
360
|
+
type Contravariant<A> = (_: A) => void;
|
|
361
|
+
type Covariant<A> = (_: never) => A;
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/container.d.ts
|
|
331
364
|
/**
|
|
332
365
|
* Type representing a factory function used to create dependency instances.
|
|
333
366
|
*
|
|
@@ -343,58 +376,23 @@ declare const InjectSource: unique symbol;
|
|
|
343
376
|
*
|
|
344
377
|
* @example Synchronous factory
|
|
345
378
|
* ```typescript
|
|
346
|
-
* const factory: Factory<DatabaseService, never> = (
|
|
379
|
+
* const factory: Factory<DatabaseService, never> = (ctx) => {
|
|
347
380
|
* return new DatabaseService('sqlite://memory');
|
|
348
381
|
* };
|
|
349
382
|
* ```
|
|
350
383
|
*
|
|
351
384
|
* @example Asynchronous factory with dependencies
|
|
352
385
|
* ```typescript
|
|
353
|
-
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (
|
|
386
|
+
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (ctx) => {
|
|
354
387
|
* const [config, db] = await Promise.all([
|
|
355
|
-
*
|
|
356
|
-
*
|
|
388
|
+
* ctx.resolve(ConfigTag),
|
|
389
|
+
* ctx.resolve(DatabaseService)
|
|
357
390
|
* ]);
|
|
358
391
|
* return new UserService(config, db);
|
|
359
392
|
* };
|
|
360
393
|
* ```
|
|
361
394
|
*/
|
|
362
|
-
type Factory<T, TReg extends AnyTag
|
|
363
|
-
/**
|
|
364
|
-
* Helper type for injecting ValueTag dependencies in constructor parameters.
|
|
365
|
-
* This allows clean specification of ValueTag dependencies while preserving
|
|
366
|
-
* the original tag information for dependency inference.
|
|
367
|
-
*
|
|
368
|
-
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
369
|
-
*
|
|
370
|
-
* @template T - A ValueTag type
|
|
371
|
-
* @returns The value type with optional phantom tag metadata for dependency inference
|
|
372
|
-
*
|
|
373
|
-
* @example
|
|
374
|
-
* ```typescript
|
|
375
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
376
|
-
*
|
|
377
|
-
* class UserService extends Tag.Class('UserService') {
|
|
378
|
-
* constructor(
|
|
379
|
-
* private db: DatabaseService, // ClassTag - works automatically
|
|
380
|
-
* private apiKey: Inject<typeof ApiKeyTag> // ValueTag - type is string, tag preserved
|
|
381
|
-
* ) {
|
|
382
|
-
* super();
|
|
383
|
-
* }
|
|
384
|
-
* }
|
|
385
|
-
* ```
|
|
386
|
-
*/
|
|
387
|
-
type Inject<T extends ValueTag<unknown, string | symbol>> = T extends ValueTag<infer V, string | symbol> ? V & {
|
|
388
|
-
readonly [InjectSource]?: T;
|
|
389
|
-
} : never;
|
|
390
|
-
/**
|
|
391
|
-
* Helper type to extract the original ValueTag from an Inject<T> type.
|
|
392
|
-
* Since InjectSource is optional, we need to check for both presence and absence.
|
|
393
|
-
* @internal
|
|
394
|
-
*/
|
|
395
|
-
type ExtractInjectTag<T> = T extends {
|
|
396
|
-
readonly [InjectSource]?: infer U;
|
|
397
|
-
} ? U : never;
|
|
395
|
+
type Factory<T, TReg extends AnyTag> = (ctx: ResolutionContext<TReg>) => PromiseOrValue<T>;
|
|
398
396
|
/**
|
|
399
397
|
* Type representing a finalizer function used to clean up dependency instances.
|
|
400
398
|
*
|
|
@@ -436,19 +434,95 @@ type ExtractInjectTag<T> = T extends {
|
|
|
436
434
|
* ```
|
|
437
435
|
*/
|
|
438
436
|
type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Type representing a complete dependency lifecycle with both factory and finalizer.
|
|
439
|
+
*
|
|
440
|
+
* This type is used when registering dependencies that need cleanup. Instead of
|
|
441
|
+
* passing separate factory and finalizer parameters, you can pass an object
|
|
442
|
+
* containing both.
|
|
443
|
+
*
|
|
444
|
+
* @template T - The dependency tag type
|
|
445
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
446
|
+
*
|
|
447
|
+
* @example Using DependencyLifecycle for registration
|
|
448
|
+
* ```typescript
|
|
449
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
450
|
+
* async connect() { return; }
|
|
451
|
+
* async disconnect() { return; }
|
|
452
|
+
* }
|
|
453
|
+
*
|
|
454
|
+
* const lifecycle: DependencyLifecycle<typeof DatabaseConnection, never> = {
|
|
455
|
+
* factory: async () => {
|
|
456
|
+
* const conn = new DatabaseConnection();
|
|
457
|
+
* await conn.connect();
|
|
458
|
+
* return conn;
|
|
459
|
+
* },
|
|
460
|
+
* finalizer: async (conn) => {
|
|
461
|
+
* await conn.disconnect();
|
|
462
|
+
* }
|
|
463
|
+
* };
|
|
464
|
+
*
|
|
465
|
+
* Container.empty().register(DatabaseConnection, lifecycle);
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
469
|
+
factory: Factory<T, TReg>;
|
|
470
|
+
finalizer: Finalizer<T>;
|
|
447
471
|
};
|
|
448
|
-
|
|
449
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Union type representing all valid dependency registration specifications.
|
|
474
|
+
*
|
|
475
|
+
* A dependency can be registered either as:
|
|
476
|
+
* - A simple factory function that creates the dependency
|
|
477
|
+
* - A complete lifecycle object with both factory and finalizer
|
|
478
|
+
*
|
|
479
|
+
* @template T - The dependency tag type
|
|
480
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
481
|
+
*
|
|
482
|
+
* @example Simple factory registration
|
|
483
|
+
* ```typescript
|
|
484
|
+
* const spec: DependencySpec<typeof UserService, never> =
|
|
485
|
+
* () => new UserService();
|
|
486
|
+
*
|
|
487
|
+
* Container.empty().register(UserService, spec);
|
|
488
|
+
* ```
|
|
489
|
+
*
|
|
490
|
+
* @example Lifecycle registration
|
|
491
|
+
* ```typescript
|
|
492
|
+
* const spec: DependencySpec<typeof DatabaseConnection, never> = {
|
|
493
|
+
* factory: () => new DatabaseConnection(),
|
|
494
|
+
* finalizer: (conn) => conn.close()
|
|
495
|
+
* };
|
|
496
|
+
*
|
|
497
|
+
* Container.empty().register(DatabaseConnection, spec);
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
type DependencySpec<T extends AnyTag, TReg extends AnyTag> = Factory<TagType<T>, TReg> | DependencyLifecycle<TagType<T>, TReg>;
|
|
501
|
+
/**
|
|
502
|
+
* Type representing the context available to factory functions during dependency resolution.
|
|
503
|
+
*
|
|
504
|
+
* This type contains only the `resolve` and `resolveAll` methods from the container, which are used to retrieve
|
|
505
|
+
* other dependencies during the creation of a service.
|
|
506
|
+
*
|
|
507
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
508
|
+
*/
|
|
509
|
+
type ResolutionContext<TReg extends AnyTag> = Pick<IContainer<TReg>, 'resolve' | 'resolveAll'>;
|
|
510
|
+
declare const ContainerTypeId: unique symbol;
|
|
511
|
+
/**
|
|
512
|
+
* Interface representing a container that can register and retrieve dependencies.
|
|
513
|
+
*
|
|
514
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
515
|
+
*/
|
|
516
|
+
interface IContainer<TReg extends AnyTag = never> {
|
|
517
|
+
readonly [ContainerTypeId]: {
|
|
518
|
+
readonly _TReg: Contravariant<TReg>;
|
|
519
|
+
};
|
|
520
|
+
register: <T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>) => IContainer<TReg | T>;
|
|
450
521
|
has(tag: AnyTag): boolean;
|
|
451
|
-
|
|
522
|
+
exists(tag: AnyTag): boolean;
|
|
523
|
+
resolve: <T extends TReg>(tag: T) => Promise<TagType<T>>;
|
|
524
|
+
resolveAll: <const T extends readonly TReg[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
525
|
+
merge<TTarget extends AnyTag>(other: IContainer<TTarget>): IContainer<TReg | TTarget>;
|
|
452
526
|
destroy(): Promise<void>;
|
|
453
527
|
}
|
|
454
528
|
/**
|
|
@@ -462,26 +536,26 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
462
536
|
*
|
|
463
537
|
* @template TReg - Union type of all registered dependency tags in this container
|
|
464
538
|
*
|
|
465
|
-
* @example Basic usage with
|
|
539
|
+
* @example Basic usage with service tags
|
|
466
540
|
* ```typescript
|
|
467
|
-
* import { container, Tag } from '
|
|
541
|
+
* import { container, Tag } from 'sandly';
|
|
468
542
|
*
|
|
469
|
-
* class DatabaseService extends Tag.
|
|
543
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
470
544
|
* query() { return 'data'; }
|
|
471
545
|
* }
|
|
472
546
|
*
|
|
473
|
-
* class UserService extends Tag.
|
|
547
|
+
* class UserService extends Tag.Service('UserService') {
|
|
474
548
|
* constructor(private db: DatabaseService) {}
|
|
475
549
|
* getUser() { return this.db.query(); }
|
|
476
550
|
* }
|
|
477
551
|
*
|
|
478
|
-
* const c =
|
|
552
|
+
* const c = Container.empty()
|
|
479
553
|
* .register(DatabaseService, () => new DatabaseService())
|
|
480
|
-
* .register(UserService, async (
|
|
481
|
-
* new UserService(await
|
|
554
|
+
* .register(UserService, async (ctx) =>
|
|
555
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
482
556
|
* );
|
|
483
557
|
*
|
|
484
|
-
* const userService = await c.
|
|
558
|
+
* const userService = await c.resolve(UserService);
|
|
485
559
|
* ```
|
|
486
560
|
*
|
|
487
561
|
* @example Usage with value tags
|
|
@@ -489,22 +563,22 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
489
563
|
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
490
564
|
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
491
565
|
*
|
|
492
|
-
* const c =
|
|
566
|
+
* const c = Container.empty()
|
|
493
567
|
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
494
568
|
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
495
569
|
*
|
|
496
|
-
* const apiKey = await c.
|
|
497
|
-
* const config = await c.
|
|
570
|
+
* const apiKey = await c.resolve(ApiKeyTag);
|
|
571
|
+
* const config = await c.resolve(ConfigTag);
|
|
498
572
|
* ```
|
|
499
573
|
*
|
|
500
574
|
* @example With finalizers for cleanup
|
|
501
575
|
* ```typescript
|
|
502
|
-
* class DatabaseConnection extends Tag.
|
|
576
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
503
577
|
* async connect() { return; }
|
|
504
578
|
* async disconnect() { return; }
|
|
505
579
|
* }
|
|
506
580
|
*
|
|
507
|
-
* const c =
|
|
581
|
+
* const c = Container.empty().register(
|
|
508
582
|
* DatabaseConnection,
|
|
509
583
|
* async () => {
|
|
510
584
|
* const conn = new DatabaseConnection();
|
|
@@ -519,43 +593,56 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
519
593
|
* ```
|
|
520
594
|
*/
|
|
521
595
|
declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
596
|
+
readonly [ContainerTypeId]: {
|
|
597
|
+
readonly _TReg: Contravariant<TReg>;
|
|
598
|
+
};
|
|
522
599
|
/**
|
|
523
600
|
* Cache of instantiated dependencies as promises.
|
|
524
601
|
* Ensures singleton behavior and supports concurrent access.
|
|
525
602
|
* @internal
|
|
526
603
|
*/
|
|
527
|
-
|
|
604
|
+
protected readonly cache: Map<AnyTag, Promise<unknown>>;
|
|
528
605
|
/**
|
|
529
606
|
* Factory functions for creating dependency instances.
|
|
530
607
|
* @internal
|
|
531
608
|
*/
|
|
532
|
-
|
|
609
|
+
protected readonly factories: Map<AnyTag, Factory<unknown, TReg>>;
|
|
533
610
|
/**
|
|
534
611
|
* Finalizer functions for cleaning up dependencies when the container is destroyed.
|
|
535
612
|
* @internal
|
|
536
613
|
*/
|
|
537
|
-
|
|
614
|
+
protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
|
|
615
|
+
/**
|
|
616
|
+
* Flag indicating whether this container has been destroyed.
|
|
617
|
+
* @internal
|
|
618
|
+
*/
|
|
619
|
+
protected isDestroyed: boolean;
|
|
620
|
+
static empty(): Container<never>;
|
|
538
621
|
/**
|
|
539
622
|
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
540
623
|
*
|
|
541
624
|
* The factory function receives the current container instance and must return the
|
|
542
625
|
* service instance (or a Promise of it). The container tracks the registration at
|
|
543
|
-
* the type level, ensuring type safety for subsequent `.
|
|
626
|
+
* the type level, ensuring type safety for subsequent `.resolve()` calls.
|
|
627
|
+
*
|
|
628
|
+
* If a dependency is already registered, this method will override it unless the
|
|
629
|
+
* dependency has already been instantiated, in which case it will throw an error.
|
|
544
630
|
*
|
|
545
631
|
* @template T - The dependency tag being registered
|
|
546
632
|
* @param tag - The dependency tag (class or value tag)
|
|
547
633
|
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
548
634
|
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
549
635
|
* @returns A new container instance with the dependency registered
|
|
550
|
-
* @throws {
|
|
636
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
637
|
+
* @throws {Error} If the dependency has already been instantiated
|
|
551
638
|
*
|
|
552
639
|
* @example Registering a simple service
|
|
553
640
|
* ```typescript
|
|
554
|
-
* class LoggerService extends Tag.
|
|
641
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
555
642
|
* log(message: string) { console.log(message); }
|
|
556
643
|
* }
|
|
557
644
|
*
|
|
558
|
-
* const c =
|
|
645
|
+
* const c = Container.empty().register(
|
|
559
646
|
* LoggerService,
|
|
560
647
|
* () => new LoggerService()
|
|
561
648
|
* );
|
|
@@ -563,26 +650,33 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
563
650
|
*
|
|
564
651
|
* @example Registering with dependencies
|
|
565
652
|
* ```typescript
|
|
566
|
-
* class UserService extends Tag.
|
|
653
|
+
* class UserService extends Tag.Service('UserService') {
|
|
567
654
|
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
568
655
|
* }
|
|
569
656
|
*
|
|
570
|
-
* const c =
|
|
657
|
+
* const c = Container.empty()
|
|
571
658
|
* .register(DatabaseService, () => new DatabaseService())
|
|
572
659
|
* .register(LoggerService, () => new LoggerService())
|
|
573
|
-
* .register(UserService, async (
|
|
660
|
+
* .register(UserService, async (ctx) =>
|
|
574
661
|
* new UserService(
|
|
575
|
-
* await
|
|
576
|
-
* await
|
|
662
|
+
* await ctx.resolve(DatabaseService),
|
|
663
|
+
* await ctx.resolve(LoggerService)
|
|
577
664
|
* )
|
|
578
665
|
* );
|
|
579
666
|
* ```
|
|
580
667
|
*
|
|
668
|
+
* @example Overriding a dependency
|
|
669
|
+
* ```typescript
|
|
670
|
+
* const c = Container.empty()
|
|
671
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
672
|
+
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
673
|
+
* ```
|
|
674
|
+
*
|
|
581
675
|
* @example Using value tags
|
|
582
676
|
* ```typescript
|
|
583
677
|
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
584
678
|
*
|
|
585
|
-
* const c =
|
|
679
|
+
* const c = Container.empty().register(
|
|
586
680
|
* ConfigTag,
|
|
587
681
|
* () => ({ apiUrl: 'https://api.example.com' })
|
|
588
682
|
* );
|
|
@@ -590,12 +684,12 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
590
684
|
*
|
|
591
685
|
* @example With finalizer for cleanup
|
|
592
686
|
* ```typescript
|
|
593
|
-
* class DatabaseConnection extends Tag.
|
|
687
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
594
688
|
* async connect() { return; }
|
|
595
689
|
* async close() { return; }
|
|
596
690
|
* }
|
|
597
691
|
*
|
|
598
|
-
* const c =
|
|
692
|
+
* const c = Container.empty().register(
|
|
599
693
|
* DatabaseConnection,
|
|
600
694
|
* async () => {
|
|
601
695
|
* const conn = new DatabaseConnection();
|
|
@@ -606,27 +700,30 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
606
700
|
* );
|
|
607
701
|
* ```
|
|
608
702
|
*/
|
|
609
|
-
register<T extends AnyTag>(tag: T,
|
|
703
|
+
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>): Container<TReg | T>;
|
|
610
704
|
/**
|
|
611
|
-
* Checks if a dependency has been
|
|
705
|
+
* Checks if a dependency has been registered in the container.
|
|
612
706
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
707
|
+
* This returns `true` if the dependency has been registered via `.register()`,
|
|
708
|
+
* regardless of whether it has been instantiated yet.
|
|
615
709
|
*
|
|
616
710
|
* @param tag - The dependency tag to check
|
|
617
|
-
* @returns `true` if the dependency has been
|
|
711
|
+
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
618
712
|
*
|
|
619
713
|
* @example
|
|
620
714
|
* ```typescript
|
|
621
|
-
* const c =
|
|
622
|
-
*
|
|
623
|
-
* console.log(c.has(DatabaseService)); // false - not instantiated yet
|
|
624
|
-
*
|
|
625
|
-
* await c.get(DatabaseService);
|
|
626
|
-
* console.log(c.has(DatabaseService)); // true - now instantiated and cached
|
|
715
|
+
* const c = Container.empty().register(DatabaseService, () => new DatabaseService());
|
|
716
|
+
* console.log(c.has(DatabaseService)); // true
|
|
627
717
|
* ```
|
|
628
718
|
*/
|
|
629
719
|
has(tag: AnyTag): boolean;
|
|
720
|
+
/**
|
|
721
|
+
* Checks if a dependency has been instantiated (cached) in the container.
|
|
722
|
+
*
|
|
723
|
+
* @param tag - The dependency tag to check
|
|
724
|
+
* @returns true if the dependency has been instantiated, false otherwise
|
|
725
|
+
*/
|
|
726
|
+
exists(tag: AnyTag): boolean;
|
|
630
727
|
/**
|
|
631
728
|
* Retrieves a dependency instance from the container, creating it if necessary.
|
|
632
729
|
*
|
|
@@ -646,10 +743,10 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
646
743
|
*
|
|
647
744
|
* @example Basic usage
|
|
648
745
|
* ```typescript
|
|
649
|
-
* const c =
|
|
746
|
+
* const c = Container.empty()
|
|
650
747
|
* .register(DatabaseService, () => new DatabaseService());
|
|
651
748
|
*
|
|
652
|
-
* const db = await c.
|
|
749
|
+
* const db = await c.resolve(DatabaseService);
|
|
653
750
|
* db.query('SELECT * FROM users');
|
|
654
751
|
* ```
|
|
655
752
|
*
|
|
@@ -657,9 +754,9 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
657
754
|
* ```typescript
|
|
658
755
|
* // All three calls will receive the same instance
|
|
659
756
|
* const [db1, db2, db3] = await Promise.all([
|
|
660
|
-
* c.
|
|
661
|
-
* c.
|
|
662
|
-
* c.
|
|
757
|
+
* c.resolve(DatabaseService),
|
|
758
|
+
* c.resolve(DatabaseService),
|
|
759
|
+
* c.resolve(DatabaseService)
|
|
663
760
|
* ]);
|
|
664
761
|
*
|
|
665
762
|
* console.log(db1 === db2 === db3); // true
|
|
@@ -667,23 +764,99 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
667
764
|
*
|
|
668
765
|
* @example Dependency injection in factories
|
|
669
766
|
* ```typescript
|
|
670
|
-
* const c =
|
|
767
|
+
* const c = Container.empty()
|
|
671
768
|
* .register(DatabaseService, () => new DatabaseService())
|
|
672
|
-
* .register(UserService, async (
|
|
673
|
-
* const db = await
|
|
769
|
+
* .register(UserService, async (ctx) => {
|
|
770
|
+
* const db = await ctx.resolve(DatabaseService);
|
|
674
771
|
* return new UserService(db);
|
|
675
772
|
* });
|
|
676
773
|
*
|
|
677
|
-
* const userService = await c.
|
|
774
|
+
* const userService = await c.resolve(UserService);
|
|
775
|
+
* ```
|
|
776
|
+
*/
|
|
777
|
+
resolve<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
778
|
+
/**
|
|
779
|
+
* Resolves multiple dependencies concurrently using Promise.all.
|
|
780
|
+
*
|
|
781
|
+
* This method takes a variable number of dependency tags and resolves all of them concurrently,
|
|
782
|
+
* returning a tuple with the resolved instances in the same order as the input tags.
|
|
783
|
+
* The method maintains all the same guarantees as the individual resolve method:
|
|
784
|
+
* singleton behavior, circular dependency detection, and proper error handling.
|
|
785
|
+
*
|
|
786
|
+
* @template T - The tuple type of dependency tags to resolve
|
|
787
|
+
* @param tags - Variable number of dependency tags to resolve
|
|
788
|
+
* @returns Promise resolving to a tuple of service instances in the same order
|
|
789
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
790
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
791
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
792
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
793
|
+
*
|
|
794
|
+
* @example Basic usage
|
|
795
|
+
* ```typescript
|
|
796
|
+
* const c = Container.empty()
|
|
797
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
798
|
+
* .register(LoggerService, () => new LoggerService());
|
|
799
|
+
*
|
|
800
|
+
* const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
|
|
801
|
+
* ```
|
|
802
|
+
*
|
|
803
|
+
* @example Mixed tag types
|
|
804
|
+
* ```typescript
|
|
805
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
806
|
+
* const c = Container.empty()
|
|
807
|
+
* .register(ApiKeyTag, () => 'secret-key')
|
|
808
|
+
* .register(UserService, () => new UserService());
|
|
809
|
+
*
|
|
810
|
+
* const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* @example Empty array
|
|
814
|
+
* ```typescript
|
|
815
|
+
* const results = await c.resolveAll(); // Returns empty array
|
|
816
|
+
* ```
|
|
817
|
+
*/
|
|
818
|
+
resolveAll<const T extends readonly TReg[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
|
|
819
|
+
/**
|
|
820
|
+
* Copies all registrations from this container to a target container.
|
|
821
|
+
*
|
|
822
|
+
* @internal
|
|
823
|
+
* @param target - The container to copy registrations to
|
|
824
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
825
|
+
*/
|
|
826
|
+
copyTo<TTarget extends AnyTag>(target: Container<TTarget>): void;
|
|
827
|
+
/**
|
|
828
|
+
* Creates a new container by merging this container's registrations with another container.
|
|
829
|
+
*
|
|
830
|
+
* This method creates a new container that contains all registrations from both containers.
|
|
831
|
+
* If there are conflicts (same dependency registered in both containers), this
|
|
832
|
+
* container's registration will take precedence.
|
|
833
|
+
*
|
|
834
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
835
|
+
* The new container starts with an empty instance cache.
|
|
836
|
+
*
|
|
837
|
+
* @param other - The container to merge with
|
|
838
|
+
* @returns A new container with combined registrations
|
|
839
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
840
|
+
*
|
|
841
|
+
* @example Merging containers
|
|
842
|
+
* ```typescript
|
|
843
|
+
* const container1 = Container.empty()
|
|
844
|
+
* .register(DatabaseService, () => new DatabaseService());
|
|
845
|
+
*
|
|
846
|
+
* const container2 = Container.empty()
|
|
847
|
+
* .register(UserService, () => new UserService());
|
|
848
|
+
*
|
|
849
|
+
* const merged = container1.merge(container2);
|
|
850
|
+
* // merged has both DatabaseService and UserService
|
|
678
851
|
* ```
|
|
679
852
|
*/
|
|
680
|
-
|
|
853
|
+
merge<TTarget extends AnyTag>(other: Container<TTarget>): Container<TReg | TTarget>;
|
|
681
854
|
/**
|
|
682
|
-
* Destroys all instantiated dependencies by calling their finalizers
|
|
855
|
+
* Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
|
|
683
856
|
*
|
|
684
|
-
* **Important:
|
|
685
|
-
*
|
|
686
|
-
*
|
|
857
|
+
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
858
|
+
* Any subsequent calls to register(), get(), or destroy() will throw a ContainerError.
|
|
859
|
+
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
687
860
|
*
|
|
688
861
|
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
689
862
|
* for maximum cleanup performance.
|
|
@@ -695,11 +868,11 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
695
868
|
* dependencies are cleaned up.
|
|
696
869
|
*
|
|
697
870
|
* @returns Promise that resolves when all cleanup is complete
|
|
698
|
-
* @throws {
|
|
871
|
+
* @throws {DependencyFinalizationError} If any finalizers fail during cleanup
|
|
699
872
|
*
|
|
700
|
-
* @example Basic cleanup
|
|
873
|
+
* @example Basic cleanup
|
|
701
874
|
* ```typescript
|
|
702
|
-
* const c =
|
|
875
|
+
* const c = Container.empty()
|
|
703
876
|
* .register(DatabaseConnection,
|
|
704
877
|
* async () => {
|
|
705
878
|
* const conn = new DatabaseConnection();
|
|
@@ -709,24 +882,32 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
709
882
|
* (conn) => conn.disconnect() // Finalizer
|
|
710
883
|
* );
|
|
711
884
|
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* await c.destroy(); // Calls conn.disconnect(), clears cache
|
|
885
|
+
* const db = await c.resolve(DatabaseConnection);
|
|
886
|
+
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
715
887
|
*
|
|
716
|
-
* //
|
|
717
|
-
*
|
|
718
|
-
*
|
|
888
|
+
* // This will throw an error
|
|
889
|
+
* try {
|
|
890
|
+
* await c.resolve(DatabaseConnection);
|
|
891
|
+
* } catch (error) {
|
|
892
|
+
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
893
|
+
* }
|
|
719
894
|
* ```
|
|
720
895
|
*
|
|
721
|
-
* @example
|
|
896
|
+
* @example Application shutdown
|
|
722
897
|
* ```typescript
|
|
723
|
-
* const
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
898
|
+
* const appContainer Container.empty
|
|
899
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
900
|
+
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
|
|
901
|
+
*
|
|
902
|
+
* // During application shutdown
|
|
903
|
+
* process.on('SIGTERM', async () => {
|
|
904
|
+
* try {
|
|
905
|
+
* await appContainer.destroy(); // Clean shutdown of all services
|
|
906
|
+
* } catch (error) {
|
|
907
|
+
* console.error('Error during shutdown:', error);
|
|
908
|
+
* }
|
|
909
|
+
* process.exit(0);
|
|
910
|
+
* });
|
|
730
911
|
* ```
|
|
731
912
|
*
|
|
732
913
|
* @example Handling cleanup errors
|
|
@@ -738,138 +919,241 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
738
919
|
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
739
920
|
* }
|
|
740
921
|
* }
|
|
741
|
-
* // Container is
|
|
922
|
+
* // Container is destroyed regardless of finalizer errors
|
|
742
923
|
* ```
|
|
743
924
|
*/
|
|
744
925
|
destroy(): Promise<void>;
|
|
745
926
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
constructor(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/errors.d.ts
|
|
929
|
+
type ErrorProps = {
|
|
930
|
+
cause?: unknown;
|
|
931
|
+
detail?: Record<string, unknown>;
|
|
932
|
+
};
|
|
933
|
+
type ErrorDump = {
|
|
934
|
+
name: string;
|
|
935
|
+
message: string;
|
|
936
|
+
stack?: string;
|
|
937
|
+
error: {
|
|
938
|
+
name: string;
|
|
939
|
+
message: string;
|
|
940
|
+
detail: Record<string, unknown>;
|
|
941
|
+
cause?: unknown;
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
declare class BaseError extends Error {
|
|
945
|
+
detail: Record<string, unknown> | undefined;
|
|
946
|
+
constructor(message: string, {
|
|
947
|
+
cause,
|
|
948
|
+
detail
|
|
949
|
+
}?: ErrorProps);
|
|
950
|
+
static ensure(error: unknown): BaseError;
|
|
951
|
+
dump(): ErrorDump;
|
|
952
|
+
dumps(): string;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Base error class for all dependency container related errors.
|
|
956
|
+
*
|
|
957
|
+
* This extends the framework's BaseError to provide consistent error handling
|
|
958
|
+
* and structured error information across the dependency injection system.
|
|
959
|
+
*
|
|
960
|
+
* @example Catching DI errors
|
|
961
|
+
* ```typescript
|
|
962
|
+
* try {
|
|
963
|
+
* await container.resolve(SomeService);
|
|
964
|
+
* } catch (error) {
|
|
965
|
+
* if (error instanceof ContainerError) {
|
|
966
|
+
* console.error('DI Error:', error.message);
|
|
967
|
+
* console.error('Details:', error.detail);
|
|
968
|
+
* }
|
|
969
|
+
* }
|
|
970
|
+
* ```
|
|
971
|
+
*/
|
|
972
|
+
declare class ContainerError extends BaseError {}
|
|
973
|
+
/**
|
|
974
|
+
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
975
|
+
*
|
|
976
|
+
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
977
|
+
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
978
|
+
* by existing dependencies.
|
|
979
|
+
*/
|
|
980
|
+
declare class DependencyAlreadyInstantiatedError extends ContainerError {}
|
|
981
|
+
/**
|
|
982
|
+
* Error thrown when attempting to use a container that has been destroyed.
|
|
983
|
+
*
|
|
984
|
+
* This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
|
|
985
|
+
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
986
|
+
* is being used after it has been destroyed.
|
|
987
|
+
*/
|
|
988
|
+
declare class ContainerDestroyedError extends ContainerError {}
|
|
989
|
+
/**
|
|
990
|
+
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
991
|
+
*
|
|
992
|
+
* This error occurs when calling `container.resolve(Tag)` for a tag that was never
|
|
993
|
+
* registered via `container.register()`. It indicates a programming error where
|
|
994
|
+
* the dependency setup is incomplete.
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* ```typescript
|
|
998
|
+
* const c = Container.empty(); // Empty container
|
|
999
|
+
*
|
|
1000
|
+
* try {
|
|
1001
|
+
* await c.resolve(UnregisteredService); // This will throw
|
|
1002
|
+
* } catch (error) {
|
|
1003
|
+
* if (error instanceof UnknownDependencyError) {
|
|
1004
|
+
* console.error('Missing dependency:', error.message);
|
|
1005
|
+
* }
|
|
1006
|
+
* }
|
|
1007
|
+
* ```
|
|
1008
|
+
*/
|
|
1009
|
+
declare class UnknownDependencyError extends ContainerError {
|
|
801
1010
|
/**
|
|
802
|
-
*
|
|
1011
|
+
* @internal
|
|
1012
|
+
* Creates an UnknownDependencyError for the given tag.
|
|
803
1013
|
*
|
|
804
|
-
*
|
|
805
|
-
* 1. Check cache in current scope
|
|
806
|
-
* 2. Check if factory exists in current scope - if so, create instance here
|
|
807
|
-
* 3. Otherwise, delegate to parent scope
|
|
808
|
-
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
1014
|
+
* @param tag - The dependency tag that wasn't found
|
|
809
1015
|
*/
|
|
810
|
-
|
|
1016
|
+
constructor(tag: AnyTag);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Error thrown when a circular dependency is detected during dependency resolution.
|
|
1020
|
+
*
|
|
1021
|
+
* This occurs when service A depends on service B, which depends on service A (directly
|
|
1022
|
+
* or through a chain of dependencies). The error includes the full dependency chain
|
|
1023
|
+
* to help identify the circular reference.
|
|
1024
|
+
*
|
|
1025
|
+
* @example Circular dependency scenario
|
|
1026
|
+
* ```typescript
|
|
1027
|
+
* class ServiceA extends Tag.Service('ServiceA') {}
|
|
1028
|
+
* class ServiceB extends Tag.Service('ServiceB') {}
|
|
1029
|
+
*
|
|
1030
|
+
* const c = Container.empty()
|
|
1031
|
+
* .register(ServiceA, async (ctx) =>
|
|
1032
|
+
* new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
|
|
1033
|
+
* )
|
|
1034
|
+
* .register(ServiceB, async (ctx) =>
|
|
1035
|
+
* new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
|
|
1036
|
+
* );
|
|
1037
|
+
*
|
|
1038
|
+
* try {
|
|
1039
|
+
* await c.resolve(ServiceA);
|
|
1040
|
+
* } catch (error) {
|
|
1041
|
+
* if (error instanceof CircularDependencyError) {
|
|
1042
|
+
* console.error('Circular dependency:', error.message);
|
|
1043
|
+
* // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
1044
|
+
* }
|
|
1045
|
+
* }
|
|
1046
|
+
* ```
|
|
1047
|
+
*/
|
|
1048
|
+
declare class CircularDependencyError extends ContainerError {
|
|
811
1049
|
/**
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
* This method ensures proper cleanup order while maintaining reusability:
|
|
815
|
-
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
816
|
-
* 2. Then calls finalizers for dependencies created in this scope
|
|
817
|
-
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
1050
|
+
* @internal
|
|
1051
|
+
* Creates a CircularDependencyError with the dependency chain information.
|
|
818
1052
|
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
1053
|
+
* @param tag - The tag where the circular dependency was detected
|
|
1054
|
+
* @param dependencyChain - The chain of dependencies that led to the circular reference
|
|
821
1055
|
*/
|
|
822
|
-
|
|
1056
|
+
constructor(tag: AnyTag, dependencyChain: AnyTag[]);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Error thrown when a dependency factory function throws an error during instantiation.
|
|
1060
|
+
*
|
|
1061
|
+
* This wraps the original error with additional context about which dependency
|
|
1062
|
+
* failed to be created. The original error is preserved as the `cause` property.
|
|
1063
|
+
*
|
|
1064
|
+
* @example Factory throwing error
|
|
1065
|
+
* ```typescript
|
|
1066
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
1067
|
+
*
|
|
1068
|
+
* const c = Container.empty().register(DatabaseService, () => {
|
|
1069
|
+
* throw new Error('Database connection failed');
|
|
1070
|
+
* });
|
|
1071
|
+
*
|
|
1072
|
+
* try {
|
|
1073
|
+
* await c.resolve(DatabaseService);
|
|
1074
|
+
* } catch (error) {
|
|
1075
|
+
* if (error instanceof DependencyCreationError) {
|
|
1076
|
+
* console.error('Failed to create:', error.message);
|
|
1077
|
+
* console.error('Original error:', error.cause);
|
|
1078
|
+
* }
|
|
1079
|
+
* }
|
|
1080
|
+
* ```
|
|
1081
|
+
*/
|
|
1082
|
+
declare class DependencyCreationError extends ContainerError {
|
|
823
1083
|
/**
|
|
824
|
-
*
|
|
1084
|
+
* @internal
|
|
1085
|
+
* Creates a DependencyCreationError wrapping the original factory error.
|
|
825
1086
|
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
1087
|
+
* @param tag - The tag of the dependency that failed to be created
|
|
1088
|
+
* @param error - The original error thrown by the factory function
|
|
828
1089
|
*/
|
|
829
|
-
|
|
1090
|
+
constructor(tag: AnyTag, error: unknown);
|
|
830
1091
|
}
|
|
831
1092
|
/**
|
|
832
|
-
*
|
|
1093
|
+
* Error thrown when one or more finalizers fail during container destruction.
|
|
833
1094
|
*
|
|
834
|
-
* This
|
|
835
|
-
*
|
|
836
|
-
*
|
|
1095
|
+
* This error aggregates multiple finalizer failures that occurred during
|
|
1096
|
+
* `container.destroy()`. Even if some finalizers fail, the container cleanup
|
|
1097
|
+
* process continues and this error contains details of all failures.
|
|
837
1098
|
*
|
|
838
|
-
* @
|
|
839
|
-
*
|
|
840
|
-
* @example
|
|
1099
|
+
* @example Handling finalization errors
|
|
841
1100
|
* ```typescript
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
* new UserService(await container.get(DatabaseService))
|
|
851
|
-
* );
|
|
852
|
-
*
|
|
853
|
-
* const userService = await c.get(UserService);
|
|
1101
|
+
* try {
|
|
1102
|
+
* await container.destroy();
|
|
1103
|
+
* } catch (error) {
|
|
1104
|
+
* if (error instanceof DependencyFinalizationError) {
|
|
1105
|
+
* console.error('Some finalizers failed');
|
|
1106
|
+
* console.error('Error details:', error.detail.errors);
|
|
1107
|
+
* }
|
|
1108
|
+
* }
|
|
854
1109
|
* ```
|
|
855
1110
|
*/
|
|
856
|
-
declare
|
|
857
|
-
|
|
1111
|
+
declare class DependencyFinalizationError extends ContainerError {
|
|
1112
|
+
/**
|
|
1113
|
+
* @internal
|
|
1114
|
+
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
1115
|
+
*
|
|
1116
|
+
* @param errors - Array of errors thrown by individual finalizers
|
|
1117
|
+
*/
|
|
1118
|
+
constructor(errors: unknown[]);
|
|
1119
|
+
}
|
|
858
1120
|
//#endregion
|
|
859
1121
|
//#region src/layer.d.ts
|
|
1122
|
+
/**
|
|
1123
|
+
* The most generic layer type that accepts any concrete layer.
|
|
1124
|
+
*/
|
|
1125
|
+
type AnyLayer = Layer<any, any>;
|
|
1126
|
+
/**
|
|
1127
|
+
* The type ID for the Layer interface.
|
|
1128
|
+
*/
|
|
1129
|
+
declare const LayerTypeId: unique symbol;
|
|
860
1130
|
/**
|
|
861
1131
|
* A dependency layer represents a reusable, composable unit of dependency registrations.
|
|
862
1132
|
* Layers allow you to organize your dependency injection setup into logical groups
|
|
863
1133
|
* that can be combined and reused across different contexts.
|
|
864
1134
|
*
|
|
1135
|
+
* ## Type Variance
|
|
1136
|
+
*
|
|
1137
|
+
* The Layer interface uses TypeScript's variance annotations to enable safe substitutability:
|
|
1138
|
+
*
|
|
1139
|
+
* ### TRequires (covariant)
|
|
1140
|
+
* A layer requiring fewer dependencies can substitute one requiring more:
|
|
1141
|
+
* - `Layer<never, X>` can be used where `Layer<A | B, X>` is expected
|
|
1142
|
+
* - Intuition: A service that needs nothing is more flexible than one that needs specific deps
|
|
1143
|
+
*
|
|
1144
|
+
* ### TProvides (contravariant)
|
|
1145
|
+
* A layer providing more services can substitute one providing fewer:
|
|
1146
|
+
* - `Layer<X, A | B>` can be used where `Layer<X, A>` is expected
|
|
1147
|
+
* - Intuition: A service that gives you extra things is compatible with expecting fewer things
|
|
1148
|
+
*
|
|
865
1149
|
* @template TRequires - The union of tags this layer requires to be satisfied by other layers
|
|
866
1150
|
* @template TProvides - The union of tags this layer provides/registers
|
|
867
1151
|
*
|
|
868
1152
|
* @example Basic layer usage
|
|
869
1153
|
* ```typescript
|
|
870
|
-
* import { layer, Tag, container } from '
|
|
1154
|
+
* import { layer, Tag, container } from 'sandly';
|
|
871
1155
|
*
|
|
872
|
-
* class DatabaseService extends Tag.
|
|
1156
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
873
1157
|
* query() { return 'data'; }
|
|
874
1158
|
* }
|
|
875
1159
|
*
|
|
@@ -879,70 +1163,126 @@ declare function scopedContainer<TScope extends Scope>(scope: TScope): ScopedCon
|
|
|
879
1163
|
* );
|
|
880
1164
|
*
|
|
881
1165
|
* // Apply the layer to a container
|
|
882
|
-
* const c =
|
|
883
|
-
* const finalContainer = databaseLayer
|
|
1166
|
+
* const c = Container.empty();
|
|
1167
|
+
* const finalContainer = databaseLayer.register(c);
|
|
884
1168
|
*
|
|
885
|
-
* const db = await finalContainer.
|
|
1169
|
+
* const db = await finalContainer.resolve(DatabaseService);
|
|
886
1170
|
* ```
|
|
887
1171
|
*
|
|
888
|
-
* @example Layer composition
|
|
1172
|
+
* @example Layer composition with variance
|
|
889
1173
|
* ```typescript
|
|
890
1174
|
* // Layer that requires DatabaseService and provides UserService
|
|
891
1175
|
* const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
|
|
892
|
-
* container.register(UserService, async (
|
|
893
|
-
* new UserService(await
|
|
1176
|
+
* container.register(UserService, async (ctx) =>
|
|
1177
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
894
1178
|
* )
|
|
895
1179
|
* );
|
|
896
1180
|
*
|
|
897
|
-
* // Compose layers: database layer
|
|
898
|
-
* const appLayer =
|
|
1181
|
+
* // Compose layers: provide database layer to user layer
|
|
1182
|
+
* const appLayer = userLayer.provide(databaseLayer);
|
|
899
1183
|
* ```
|
|
900
1184
|
*/
|
|
901
|
-
interface Layer<TRequires extends AnyTag
|
|
1185
|
+
interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
|
|
1186
|
+
readonly [LayerTypeId]?: {
|
|
1187
|
+
readonly _TRequires: Covariant<TRequires>;
|
|
1188
|
+
readonly _TProvides: Contravariant<TProvides>;
|
|
1189
|
+
};
|
|
902
1190
|
/**
|
|
903
1191
|
* Applies this layer's registrations to the given container.
|
|
904
1192
|
*
|
|
905
|
-
*
|
|
906
|
-
* @returns A new container with this layer's dependencies registered
|
|
1193
|
+
* ## Generic Container Support
|
|
907
1194
|
*
|
|
908
|
-
*
|
|
1195
|
+
* The signature uses `TContainer extends AnyTag` to accept containers with any existing
|
|
1196
|
+
* services while preserving type information. The container must provide at least this
|
|
1197
|
+
* layer's requirements (`TRequires`) but can have additional services (`TContainer`).
|
|
1198
|
+
*
|
|
1199
|
+
* Result container has: `TRequires | TContainer | TProvides` - everything that was
|
|
1200
|
+
* already there plus this layer's new provisions.
|
|
1201
|
+
*
|
|
1202
|
+
* @param container - The container to register dependencies into (must satisfy TRequires)
|
|
1203
|
+
* @returns A new container with this layer's dependencies registered and all existing services preserved
|
|
1204
|
+
*
|
|
1205
|
+
* @example Basic usage
|
|
909
1206
|
* ```typescript
|
|
910
|
-
* const
|
|
911
|
-
* const updatedContainer = myLayer.register(
|
|
1207
|
+
* const c = Container.empty();
|
|
1208
|
+
* const updatedContainer = myLayer.register(c);
|
|
1209
|
+
* ```
|
|
1210
|
+
*
|
|
1211
|
+
* @example With existing services preserved
|
|
1212
|
+
* ```typescript
|
|
1213
|
+
* const baseContainer = Container.empty()
|
|
1214
|
+
* .register(ExistingService, () => new ExistingService());
|
|
1215
|
+
*
|
|
1216
|
+
* const enhanced = myLayer.register(baseContainer);
|
|
1217
|
+
* // Enhanced container has both ExistingService and myLayer's provisions
|
|
912
1218
|
* ```
|
|
913
1219
|
*/
|
|
914
|
-
register: <
|
|
1220
|
+
register: <TContainer extends AnyTag>(container: IContainer<TRequires | TContainer>) => IContainer<TRequires | TContainer | TProvides>;
|
|
915
1221
|
/**
|
|
916
|
-
*
|
|
917
|
-
* provisions satisfy
|
|
918
|
-
* flow from source → target.
|
|
1222
|
+
* Provides a dependency layer to this layer, creating a pipeline where the dependency layer's
|
|
1223
|
+
* provisions satisfy this layer's requirements. This creates a dependency flow from dependency → this.
|
|
919
1224
|
*
|
|
920
|
-
* Type-safe:
|
|
1225
|
+
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
921
1226
|
* provisions and any remaining external requirements.
|
|
922
1227
|
*
|
|
923
|
-
* @template
|
|
924
|
-
* @template
|
|
925
|
-
* @param
|
|
926
|
-
* @returns A new composed layer
|
|
1228
|
+
* @template TDepRequires - What the dependency layer requires
|
|
1229
|
+
* @template TDepProvides - What the dependency layer provides
|
|
1230
|
+
* @param dependency - The layer to provide as a dependency
|
|
1231
|
+
* @returns A new composed layer that only exposes this layer's provisions
|
|
927
1232
|
*
|
|
928
1233
|
* @example Simple composition
|
|
929
1234
|
* ```typescript
|
|
930
1235
|
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
931
1236
|
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
932
1237
|
*
|
|
933
|
-
* //
|
|
934
|
-
* const infraLayer =
|
|
1238
|
+
* // Provide config to database layer
|
|
1239
|
+
* const infraLayer = dbLayer.provide(configLayer);
|
|
1240
|
+
* ```
|
|
1241
|
+
*
|
|
1242
|
+
* @example Multi-level composition (reads naturally left-to-right)
|
|
1243
|
+
* ```typescript
|
|
1244
|
+
* const appLayer = apiLayer
|
|
1245
|
+
* .provide(serviceLayer)
|
|
1246
|
+
* .provide(databaseLayer)
|
|
1247
|
+
* .provide(configLayer);
|
|
1248
|
+
* ```
|
|
1249
|
+
*/
|
|
1250
|
+
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
|
|
1251
|
+
/**
|
|
1252
|
+
* Provides a dependency layer to this layer and merges the provisions.
|
|
1253
|
+
* Unlike `.provide()`, this method includes both this layer's provisions and the dependency layer's
|
|
1254
|
+
* provisions in the result type. This is useful when you want to expose services from both layers.
|
|
1255
|
+
*
|
|
1256
|
+
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
1257
|
+
* provisions and any remaining external requirements.
|
|
1258
|
+
*
|
|
1259
|
+
* @template TDepRequires - What the dependency layer requires
|
|
1260
|
+
* @template TDepProvides - What the dependency layer provides
|
|
1261
|
+
* @param dependency - The layer to provide as a dependency
|
|
1262
|
+
* @returns A new composed layer that provides services from both layers
|
|
1263
|
+
*
|
|
1264
|
+
* @example Providing with merged provisions
|
|
1265
|
+
* ```typescript
|
|
1266
|
+
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
1267
|
+
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
1268
|
+
*
|
|
1269
|
+
* // Provide config to database layer, and both services are available
|
|
1270
|
+
* const infraLayer = dbLayer.provideMerge(configLayer);
|
|
1271
|
+
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
935
1272
|
* ```
|
|
936
1273
|
*
|
|
937
|
-
* @example
|
|
1274
|
+
* @example Difference from .provide()
|
|
938
1275
|
* ```typescript
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
1276
|
+
* // .provide() only exposes this layer's provisions:
|
|
1277
|
+
* const withProvide = dbLayer.provide(configLayer);
|
|
1278
|
+
* // Type: Layer<never, typeof DatabaseService>
|
|
1279
|
+
*
|
|
1280
|
+
* // .provideMerge() exposes both layers' provisions:
|
|
1281
|
+
* const withProvideMerge = dbLayer.provideMerge(configLayer);
|
|
1282
|
+
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
943
1283
|
* ```
|
|
944
1284
|
*/
|
|
945
|
-
|
|
1285
|
+
provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
|
|
946
1286
|
/**
|
|
947
1287
|
* Merges this layer with another layer, combining their requirements and provisions.
|
|
948
1288
|
* This is useful for combining independent layers that don't have a dependency
|
|
@@ -959,42 +1299,33 @@ interface Layer<TRequires extends AnyTag = never, TProvides extends AnyTag = nev
|
|
|
959
1299
|
* const loggingLayer = layer<never, typeof LoggerService>(...);
|
|
960
1300
|
*
|
|
961
1301
|
* // Combine infrastructure layers
|
|
962
|
-
* const infraLayer = persistenceLayer
|
|
1302
|
+
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
963
1303
|
* ```
|
|
964
1304
|
*
|
|
965
1305
|
* @example Building complex layer combinations
|
|
966
1306
|
* ```typescript
|
|
967
|
-
* const appInfraLayer = persistenceLayer
|
|
968
|
-
* .
|
|
969
|
-
* .
|
|
1307
|
+
* const appInfraLayer = persistenceLayer
|
|
1308
|
+
* .merge(messagingLayer)
|
|
1309
|
+
* .merge(observabilityLayer);
|
|
970
1310
|
* ```
|
|
971
1311
|
*/
|
|
972
|
-
|
|
1312
|
+
merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
|
|
973
1313
|
}
|
|
974
|
-
/**
|
|
975
|
-
* A factory function for creating layers.
|
|
976
|
-
*
|
|
977
|
-
* @template TRequires - The union of tags this layer requires
|
|
978
|
-
* @template TProvides - The union of tags this layer provides
|
|
979
|
-
* @template TParams - Optional parameters that can be passed to configure the layer
|
|
980
|
-
*/
|
|
981
|
-
type LayerFactory<TRequires extends AnyTag, TProvides extends AnyTag, TParams = undefined> = TParams extends undefined ? () => Layer<TRequires, TProvides> : (params: TParams) => Layer<TRequires, TProvides>;
|
|
982
1314
|
/**
|
|
983
1315
|
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
984
1316
|
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
985
1317
|
*
|
|
986
1318
|
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
987
1319
|
* @template TProvides - The union of dependency tags this layer registers/provides
|
|
988
|
-
* @template TParams - Optional parameters that can be passed to configure the layer
|
|
989
1320
|
*
|
|
990
|
-
* @param register - Function that performs the dependency registrations. Receives a container
|
|
991
|
-
* @returns
|
|
1321
|
+
* @param register - Function that performs the dependency registrations. Receives a container.
|
|
1322
|
+
* @returns The layer instance.
|
|
992
1323
|
*
|
|
993
|
-
* @example Simple layer
|
|
1324
|
+
* @example Simple layer
|
|
994
1325
|
* ```typescript
|
|
995
|
-
* import { layer, Tag } from '
|
|
1326
|
+
* import { layer, Tag } from 'sandly';
|
|
996
1327
|
*
|
|
997
|
-
* class DatabaseService extends Tag.
|
|
1328
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
998
1329
|
* constructor(private url: string = 'sqlite://memory') {}
|
|
999
1330
|
* query() { return 'data'; }
|
|
1000
1331
|
* }
|
|
@@ -1005,37 +1336,7 @@ type LayerFactory<TRequires extends AnyTag, TProvides extends AnyTag, TParams =
|
|
|
1005
1336
|
* );
|
|
1006
1337
|
*
|
|
1007
1338
|
* // Usage
|
|
1008
|
-
* const dbLayerInstance = databaseLayer
|
|
1009
|
-
* ```
|
|
1010
|
-
*
|
|
1011
|
-
* @example Layer with dependencies
|
|
1012
|
-
* ```typescript
|
|
1013
|
-
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
1014
|
-
*
|
|
1015
|
-
* // Layer that requires ConfigTag and provides DatabaseService
|
|
1016
|
-
* const databaseLayer = layer<typeof ConfigTag, typeof DatabaseService>((container) =>
|
|
1017
|
-
* container.register(DatabaseService, async (c) => {
|
|
1018
|
-
* const config = await c.get(ConfigTag);
|
|
1019
|
-
* return new DatabaseService(config.dbUrl);
|
|
1020
|
-
* })
|
|
1021
|
-
* );
|
|
1022
|
-
* ```
|
|
1023
|
-
*
|
|
1024
|
-
* @example Parameterized layer
|
|
1025
|
-
* ```typescript
|
|
1026
|
-
* interface DatabaseConfig {
|
|
1027
|
-
* host: string;
|
|
1028
|
-
* port: number;
|
|
1029
|
-
* }
|
|
1030
|
-
*
|
|
1031
|
-
* // Layer that takes configuration parameters
|
|
1032
|
-
* const databaseLayer = layer<never, typeof DatabaseService, DatabaseConfig>(
|
|
1033
|
-
* (container, config) =>
|
|
1034
|
-
* container.register(DatabaseService, () => new DatabaseService(config))
|
|
1035
|
-
* );
|
|
1036
|
-
*
|
|
1037
|
-
* // Usage with parameters
|
|
1038
|
-
* const dbLayerInstance = databaseLayer({ host: 'localhost', port: 5432 });
|
|
1339
|
+
* const dbLayerInstance = databaseLayer;
|
|
1039
1340
|
* ```
|
|
1040
1341
|
*
|
|
1041
1342
|
* @example Complex application layer structure
|
|
@@ -1049,37 +1350,45 @@ type LayerFactory<TRequires extends AnyTag, TProvides extends AnyTag, TParams =
|
|
|
1049
1350
|
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
1050
1351
|
* (container) =>
|
|
1051
1352
|
* container
|
|
1052
|
-
* .register(DatabaseService, async (
|
|
1053
|
-
* .register(CacheService, async (
|
|
1353
|
+
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
|
|
1354
|
+
* .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
|
|
1054
1355
|
* );
|
|
1055
1356
|
*
|
|
1056
1357
|
* // Service layer (requires infrastructure)
|
|
1057
1358
|
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
1058
1359
|
* (container) =>
|
|
1059
|
-
* container.register(UserService, async (
|
|
1060
|
-
* new UserService(await
|
|
1360
|
+
* container.register(UserService, async (ctx) =>
|
|
1361
|
+
* new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
|
|
1061
1362
|
* )
|
|
1062
1363
|
* );
|
|
1063
1364
|
*
|
|
1064
1365
|
* // Compose the complete application
|
|
1065
|
-
* const appLayer =
|
|
1366
|
+
* const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
|
|
1066
1367
|
* ```
|
|
1067
1368
|
*/
|
|
1068
|
-
declare function layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never
|
|
1369
|
+
declare function layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never>(register: <TContainer extends AnyTag>(container: IContainer<TRequires | TContainer>) => IContainer<TRequires | TContainer | TProvides>): Layer<TRequires, TProvides>;
|
|
1069
1370
|
/**
|
|
1070
1371
|
* Helper type that extracts the union of all requirements from an array of layers.
|
|
1071
|
-
* Used by Layer.
|
|
1372
|
+
* Used by Layer.mergeAll() to compute the correct requirement type for the merged layer.
|
|
1373
|
+
*
|
|
1374
|
+
* Works with AnyLayer[] constraint which accepts any concrete layer through variance:
|
|
1375
|
+
* - Layer<never, X> → extracts `never` (no requirements)
|
|
1376
|
+
* - Layer<A | B, Y> → extracts `A | B` (specific requirements)
|
|
1072
1377
|
*
|
|
1073
1378
|
* @internal
|
|
1074
1379
|
*/
|
|
1075
|
-
type UnionOfRequires<T extends readonly
|
|
1380
|
+
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
|
|
1076
1381
|
/**
|
|
1077
1382
|
* Helper type that extracts the union of all provisions from an array of layers.
|
|
1078
|
-
* Used by Layer.
|
|
1383
|
+
* Used by Layer.mergeAll() to compute the correct provision type for the merged layer.
|
|
1384
|
+
*
|
|
1385
|
+
* Works with AnyLayer[] constraint which accepts any concrete layer through variance:
|
|
1386
|
+
* - Layer<X, never> → extracts `never` (no provisions)
|
|
1387
|
+
* - Layer<Y, A | B> → extracts `A | B` (specific provisions)
|
|
1079
1388
|
*
|
|
1080
1389
|
* @internal
|
|
1081
1390
|
*/
|
|
1082
|
-
type UnionOfProvides<T extends readonly
|
|
1391
|
+
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
|
|
1083
1392
|
/**
|
|
1084
1393
|
* Utility object containing helper functions for working with layers.
|
|
1085
1394
|
*/
|
|
@@ -1092,43 +1401,58 @@ declare const Layer: {
|
|
|
1092
1401
|
*
|
|
1093
1402
|
* @example
|
|
1094
1403
|
* ```typescript
|
|
1095
|
-
* import { Layer } from '
|
|
1404
|
+
* import { Layer } from 'sandly';
|
|
1096
1405
|
*
|
|
1097
1406
|
* const baseLayer = Layer.empty();
|
|
1098
1407
|
* const appLayer = baseLayer
|
|
1099
|
-
* .
|
|
1100
|
-
* .
|
|
1408
|
+
* .merge(configLayer)
|
|
1409
|
+
* .merge(serviceLayer);
|
|
1101
1410
|
* ```
|
|
1102
1411
|
*/
|
|
1103
|
-
empty(): Layer
|
|
1412
|
+
empty(): Layer<never, never>;
|
|
1104
1413
|
/**
|
|
1105
1414
|
* Merges multiple layers at once in a type-safe way.
|
|
1106
|
-
* This is equivalent to chaining `.
|
|
1415
|
+
* This is equivalent to chaining `.merge()` calls but more convenient for multiple layers.
|
|
1416
|
+
*
|
|
1417
|
+
* ## Type Safety with Variance
|
|
1418
|
+
*
|
|
1419
|
+
* Uses the AnyLayer constraint (Layer<never, AnyTag>) which accepts any concrete layer
|
|
1420
|
+
* through the Layer interface's variance annotations:
|
|
1421
|
+
*
|
|
1422
|
+
* - **Contravariant TRequires**: Layer<typeof ServiceA, X> can be passed because requiring
|
|
1423
|
+
* ServiceA is more restrictive than requiring `never` (nothing)
|
|
1424
|
+
* - **Covariant TProvides**: Layer<Y, typeof ServiceB> can be passed because providing
|
|
1425
|
+
* ServiceB is compatible with the general `AnyTag` type
|
|
1426
|
+
*
|
|
1427
|
+
* The return type correctly extracts and unions the actual requirement/provision types
|
|
1428
|
+
* from all input layers, preserving full type safety.
|
|
1107
1429
|
*
|
|
1108
1430
|
* All layers are merged in order, combining their requirements and provisions.
|
|
1109
1431
|
* The resulting layer requires the union of all input layer requirements and
|
|
1110
1432
|
* provides the union of all input layer provisions.
|
|
1111
1433
|
*
|
|
1112
|
-
* @template T - The tuple type of layers to merge
|
|
1434
|
+
* @template T - The tuple type of layers to merge (constrained to AnyLayer for variance)
|
|
1113
1435
|
* @param layers - At least 2 layers to merge together
|
|
1114
|
-
* @returns A new layer that combines all input layers
|
|
1436
|
+
* @returns A new layer that combines all input layers with correct union types
|
|
1115
1437
|
*
|
|
1116
|
-
* @example Basic usage
|
|
1438
|
+
* @example Basic usage with different layer types
|
|
1117
1439
|
* ```typescript
|
|
1118
|
-
* import { Layer } from '
|
|
1440
|
+
* import { Layer } from 'sandly';
|
|
1119
1441
|
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1442
|
+
* // These all have different types but work thanks to variance:
|
|
1443
|
+
* const dbLayer = layer<never, typeof DatabaseService>(...); // no requirements
|
|
1444
|
+
* const userLayer = layer<typeof DatabaseService, typeof UserService>(...); // requires DB
|
|
1445
|
+
* const configLayer = layer<never, typeof ConfigService>(...); // no requirements
|
|
1446
|
+
*
|
|
1447
|
+
* const infraLayer = Layer.mergeAll(dbLayer, userLayer, configLayer);
|
|
1448
|
+
* // Type: Layer<typeof DatabaseService, typeof DatabaseService | typeof UserService | typeof ConfigService>
|
|
1125
1449
|
* ```
|
|
1126
1450
|
*
|
|
1127
|
-
* @example Equivalent to chaining .
|
|
1451
|
+
* @example Equivalent to chaining .merge()
|
|
1128
1452
|
* ```typescript
|
|
1129
1453
|
* // These are equivalent:
|
|
1130
|
-
* const layer1 = Layer.
|
|
1131
|
-
* const layer2 = layerA
|
|
1454
|
+
* const layer1 = Layer.mergeAll(layerA, layerB, layerC);
|
|
1455
|
+
* const layer2 = layerA.merge(layerB).merge(layerC);
|
|
1132
1456
|
* ```
|
|
1133
1457
|
*
|
|
1134
1458
|
* @example Building infrastructure layers
|
|
@@ -1138,79 +1462,211 @@ declare const Layer: {
|
|
|
1138
1462
|
* const observabilityLayer = layer<never, typeof Logger | typeof Metrics>(...);
|
|
1139
1463
|
*
|
|
1140
1464
|
* // Merge all infrastructure concerns into one layer
|
|
1141
|
-
* const infraLayer = Layer.
|
|
1142
|
-
* persistenceLayer
|
|
1143
|
-
* messagingLayer
|
|
1144
|
-
* observabilityLayer
|
|
1465
|
+
* const infraLayer = Layer.mergeAll(
|
|
1466
|
+
* persistenceLayer,
|
|
1467
|
+
* messagingLayer,
|
|
1468
|
+
* observabilityLayer
|
|
1145
1469
|
* );
|
|
1146
1470
|
*
|
|
1147
|
-
* //
|
|
1471
|
+
* // Result type: Layer<never, DatabaseService | CacheService | MessageQueue | Logger | Metrics>
|
|
1148
1472
|
* ```
|
|
1149
1473
|
*/
|
|
1150
|
-
|
|
1474
|
+
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
1475
|
+
/**
|
|
1476
|
+
* Merges exactly two layers, combining their requirements and provisions.
|
|
1477
|
+
* This is similar to the `.merge()` method but available as a static function.
|
|
1478
|
+
*
|
|
1479
|
+
* @template TRequires1 - What the first layer requires
|
|
1480
|
+
* @template TProvides1 - What the first layer provides
|
|
1481
|
+
* @template TRequires2 - What the second layer requires
|
|
1482
|
+
* @template TProvides2 - What the second layer provides
|
|
1483
|
+
* @param layer1 - The first layer to merge
|
|
1484
|
+
* @param layer2 - The second layer to merge
|
|
1485
|
+
* @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
|
|
1486
|
+
*
|
|
1487
|
+
* @example Merging two layers
|
|
1488
|
+
* ```typescript
|
|
1489
|
+
* import { Layer } from 'sandly';
|
|
1490
|
+
*
|
|
1491
|
+
* const dbLayer = layer<never, typeof DatabaseService>(...);
|
|
1492
|
+
* const cacheLayer = layer<never, typeof CacheService>(...);
|
|
1493
|
+
*
|
|
1494
|
+
* const persistenceLayer = Layer.merge(dbLayer, cacheLayer);
|
|
1495
|
+
* // Type: Layer<never, typeof DatabaseService | typeof CacheService>
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
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>;
|
|
1151
1499
|
};
|
|
1152
1500
|
//#endregion
|
|
1153
|
-
//#region src/
|
|
1501
|
+
//#region src/scoped-container.d.ts
|
|
1502
|
+
type Scope = string | symbol;
|
|
1503
|
+
declare class ScopedContainer<TReg extends AnyTag> extends Container<TReg> {
|
|
1504
|
+
readonly scope: Scope;
|
|
1505
|
+
private parent;
|
|
1506
|
+
private readonly children;
|
|
1507
|
+
protected constructor(parent: IContainer<TReg> | null, scope: Scope);
|
|
1508
|
+
static empty(scope: Scope): ScopedContainer<never>;
|
|
1509
|
+
/**
|
|
1510
|
+
* Registers a dependency in the scoped container.
|
|
1511
|
+
*
|
|
1512
|
+
* Overrides the base implementation to return ScopedContainer type
|
|
1513
|
+
* for proper method chaining support.
|
|
1514
|
+
*/
|
|
1515
|
+
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>): ScopedContainer<TReg | T>;
|
|
1516
|
+
/**
|
|
1517
|
+
* Checks if a dependency has been registered in this scope or any parent scope.
|
|
1518
|
+
*
|
|
1519
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
1520
|
+
* Returns true if the dependency has been registered somewhere in the scope hierarchy.
|
|
1521
|
+
*/
|
|
1522
|
+
has(tag: AnyTag): boolean;
|
|
1523
|
+
/**
|
|
1524
|
+
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
1525
|
+
*
|
|
1526
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
1527
|
+
* Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
|
|
1528
|
+
*/
|
|
1529
|
+
exists(tag: AnyTag): boolean;
|
|
1530
|
+
/**
|
|
1531
|
+
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
1532
|
+
*
|
|
1533
|
+
* Resolution strategy:
|
|
1534
|
+
* 1. Check cache in current scope
|
|
1535
|
+
* 2. Check if factory exists in current scope - if so, create instance here
|
|
1536
|
+
* 3. Otherwise, delegate to parent scope
|
|
1537
|
+
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
1538
|
+
*/
|
|
1539
|
+
resolve<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
1540
|
+
/**
|
|
1541
|
+
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
1542
|
+
*
|
|
1543
|
+
* This method ensures proper cleanup order while maintaining reusability:
|
|
1544
|
+
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
1545
|
+
* 2. Then calls finalizers for dependencies created in this scope
|
|
1546
|
+
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
1547
|
+
*
|
|
1548
|
+
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
1549
|
+
* before their dependents.
|
|
1550
|
+
*/
|
|
1551
|
+
destroy(): Promise<void>;
|
|
1552
|
+
/**
|
|
1553
|
+
* Creates a new scoped container by merging this container's registrations with another container.
|
|
1554
|
+
*
|
|
1555
|
+
* This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
|
|
1556
|
+
* The resulting scoped container contains all registrations from both containers and becomes a root scope
|
|
1557
|
+
* (no parent) with the scope name from this container.
|
|
1558
|
+
*
|
|
1559
|
+
* @param other - The container to merge with
|
|
1560
|
+
* @returns A new ScopedContainer with combined registrations
|
|
1561
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
1562
|
+
*/
|
|
1563
|
+
merge<TTarget extends AnyTag>(other: Container<TTarget>): ScopedContainer<TReg | TTarget>;
|
|
1564
|
+
/**
|
|
1565
|
+
* Creates a child scoped container.
|
|
1566
|
+
*
|
|
1567
|
+
* Child containers inherit access to parent dependencies but maintain
|
|
1568
|
+
* their own scope for new registrations and instance caching.
|
|
1569
|
+
*/
|
|
1570
|
+
child(scope: Scope): ScopedContainer<TReg>;
|
|
1571
|
+
}
|
|
1154
1572
|
/**
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1573
|
+
* Converts a regular container into a scoped container, copying all registrations.
|
|
1574
|
+
*
|
|
1575
|
+
* This function creates a new ScopedContainer instance and copies all factory functions
|
|
1576
|
+
* and finalizers from the source container. The resulting scoped container becomes a root
|
|
1577
|
+
* scope (no parent) with all the same dependency registrations.
|
|
1578
|
+
*
|
|
1579
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
1580
|
+
* The new scoped container starts with an empty instance cache.
|
|
1581
|
+
*
|
|
1582
|
+
* @param container - The container to convert to a scoped container
|
|
1583
|
+
* @param scope - A string or symbol identifier for this scope (used for debugging)
|
|
1584
|
+
* @returns A new ScopedContainer instance with all registrations copied from the source container
|
|
1585
|
+
* @throws {ContainerDestroyedError} If the source container has been destroyed
|
|
1586
|
+
*
|
|
1587
|
+
* @example Converting a regular container to scoped
|
|
1588
|
+
* ```typescript
|
|
1589
|
+
* import { container, scoped } from 'sandly';
|
|
1590
|
+
*
|
|
1591
|
+
* const appContainer = Container.empty()
|
|
1592
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
1593
|
+
* .register(ConfigService, () => new ConfigService());
|
|
1594
|
+
*
|
|
1595
|
+
* const scopedAppContainer = scoped(appContainer, 'app');
|
|
1596
|
+
*
|
|
1597
|
+
* // Create child scopes
|
|
1598
|
+
* const requestContainer = scopedAppContainer.child('request');
|
|
1599
|
+
* ```
|
|
1600
|
+
*
|
|
1601
|
+
* @example Copying complex registrations
|
|
1602
|
+
* ```typescript
|
|
1603
|
+
* const baseContainer = Container.empty()
|
|
1604
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
1605
|
+
* .register(UserService, {
|
|
1606
|
+
* factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
|
|
1607
|
+
* finalizer: (service) => service.cleanup()
|
|
1608
|
+
* });
|
|
1609
|
+
*
|
|
1610
|
+
* const scopedContainer = scoped(baseContainer, 'app');
|
|
1611
|
+
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
1612
|
+
* ```
|
|
1157
1613
|
*/
|
|
1158
|
-
|
|
1614
|
+
declare function scoped<TReg extends AnyTag>(container: Container<TReg>, scope: Scope): ScopedContainer<TReg>;
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region src/service.d.ts
|
|
1159
1617
|
/**
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1618
|
+
* Extracts constructor parameter types from a ServiceTag.
|
|
1619
|
+
* Only parameters that extend AnyTag are considered as dependencies.
|
|
1620
|
+
* @internal
|
|
1162
1621
|
*/
|
|
1163
|
-
type
|
|
1164
|
-
readonly [TagId]: infer Id;
|
|
1165
|
-
} ? Id extends string | symbol ? TaggedClass<T, Id> : never : never;
|
|
1622
|
+
type ConstructorParams<T extends ServiceTag<TagId, unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1166
1623
|
/**
|
|
1167
|
-
* Extracts
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1624
|
+
* Extracts only dependency tags from a constructor parameter list.
|
|
1625
|
+
* Filters out non‑DI parameters.
|
|
1626
|
+
*
|
|
1627
|
+
* Example:
|
|
1628
|
+
* [DatabaseService, Inject<typeof ConfigTag>, number]
|
|
1629
|
+
* → typeof DatabaseService | typeof ConfigTag
|
|
1630
|
+
* @internal
|
|
1170
1631
|
*/
|
|
1171
|
-
type
|
|
1172
|
-
readonly [
|
|
1173
|
-
} ?
|
|
1632
|
+
type ExtractConstructorDeps<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1633
|
+
readonly [ServiceTagIdKey]: infer Id;
|
|
1634
|
+
} ? Id extends TagId ? ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
|
|
1174
1635
|
/**
|
|
1175
|
-
*
|
|
1636
|
+
* Produces an ordered tuple of constructor parameters
|
|
1637
|
+
* where dependency parameters are replaced with their tag types,
|
|
1638
|
+
* while non‑DI parameters are preserved as‑is.
|
|
1639
|
+
* @internal
|
|
1176
1640
|
*/
|
|
1177
1641
|
|
|
1178
1642
|
/**
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
* This is used to determine what dependencies a service requires.
|
|
1643
|
+
* Union of all dependency tags a ServiceTag constructor requires.
|
|
1644
|
+
* Filters out non‑DI parameters.
|
|
1182
1645
|
*/
|
|
1183
|
-
type ServiceDependencies<T extends
|
|
1646
|
+
type ServiceDependencies<T extends ServiceTag<TagId, unknown>> = ExtractConstructorDeps<ConstructorParams<T>> extends AnyTag ? ExtractConstructorDeps<ConstructorParams<T>> : never;
|
|
1184
1647
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
* For ValueTag services, there are no dependencies since they don't have constructors.
|
|
1648
|
+
* Ordered tuple of dependency tags (and other constructor params)
|
|
1649
|
+
* inferred from a ServiceTag’s constructor.
|
|
1188
1650
|
*/
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* The tag that this service represents (ClassTag or ValueTag)
|
|
1192
|
-
*/
|
|
1193
|
-
readonly serviceClass: T;
|
|
1194
|
-
}
|
|
1651
|
+
|
|
1195
1652
|
/**
|
|
1196
|
-
* Creates a service layer from any tag type (
|
|
1653
|
+
* Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
|
|
1197
1654
|
*
|
|
1198
|
-
* For
|
|
1655
|
+
* For ServiceTag services:
|
|
1199
1656
|
* - Dependencies are automatically inferred from constructor parameters
|
|
1200
1657
|
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
1201
1658
|
*
|
|
1202
1659
|
* For ValueTag services:
|
|
1203
1660
|
* - No constructor dependencies are needed since they don't have constructors
|
|
1204
1661
|
*
|
|
1205
|
-
* @template T - The tag representing the service (
|
|
1206
|
-
* @
|
|
1207
|
-
* @param
|
|
1208
|
-
* @
|
|
1209
|
-
* @returns A factory function that creates a service layer
|
|
1662
|
+
* @template T - The tag representing the service (ServiceTag or ValueTag)
|
|
1663
|
+
* @param tag - The tag (ServiceTag or ValueTag)
|
|
1664
|
+
* @param factory - Factory function for service instantiation with container
|
|
1665
|
+
* @returns The service layer
|
|
1210
1666
|
*
|
|
1211
1667
|
* @example Simple service without dependencies
|
|
1212
1668
|
* ```typescript
|
|
1213
|
-
* class LoggerService extends Tag.
|
|
1669
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
1214
1670
|
* log(message: string) { console.log(message); }
|
|
1215
1671
|
* }
|
|
1216
1672
|
*
|
|
@@ -1219,11 +1675,11 @@ interface Service<T extends AnyTag> extends Layer<ServiceDependencies<T>, T> {
|
|
|
1219
1675
|
*
|
|
1220
1676
|
* @example Service with dependencies
|
|
1221
1677
|
* ```typescript
|
|
1222
|
-
* class DatabaseService extends Tag.
|
|
1678
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1223
1679
|
* query() { return []; }
|
|
1224
1680
|
* }
|
|
1225
1681
|
*
|
|
1226
|
-
* class UserService extends Tag.
|
|
1682
|
+
* class UserService extends Tag.Service('UserService') {
|
|
1227
1683
|
* constructor(private db: DatabaseService) {
|
|
1228
1684
|
* super();
|
|
1229
1685
|
* }
|
|
@@ -1231,25 +1687,128 @@ interface Service<T extends AnyTag> extends Layer<ServiceDependencies<T>, T> {
|
|
|
1231
1687
|
* getUsers() { return this.db.query(); }
|
|
1232
1688
|
* }
|
|
1233
1689
|
*
|
|
1234
|
-
* const userService = service(UserService, async (
|
|
1235
|
-
* new UserService(await
|
|
1690
|
+
* const userService = service(UserService, async (ctx) =>
|
|
1691
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
1236
1692
|
* );
|
|
1237
1693
|
* ```
|
|
1694
|
+
*/
|
|
1695
|
+
declare function service<T extends ServiceTag<TagId, unknown>>(tag: T, spec: DependencySpec<T, ServiceDependencies<T>>): Layer<ServiceDependencies<T>, T>;
|
|
1696
|
+
/**
|
|
1697
|
+
* Creates a service layer with automatic dependency injection by inferring constructor parameters.
|
|
1698
|
+
*
|
|
1699
|
+
* This is a convenience function that automatically resolves constructor dependencies and passes
|
|
1700
|
+
* both DI-managed dependencies and static values to the service constructor in the correct order.
|
|
1701
|
+
* It eliminates the need to manually write factory functions for services with constructor dependencies.
|
|
1702
|
+
*
|
|
1703
|
+
* @template T - The ServiceTag representing the service class
|
|
1704
|
+
* @param tag - The service tag (must be a ServiceTag, not a ValueTag)
|
|
1705
|
+
* @param deps - Tuple of constructor parameters in order - mix of dependency tags and static values
|
|
1706
|
+
* @param finalizer - Optional cleanup function called when the container is destroyed
|
|
1707
|
+
* @returns A service layer that automatically handles dependency injection
|
|
1708
|
+
*
|
|
1709
|
+
* @example Simple service with dependencies
|
|
1710
|
+
* ```typescript
|
|
1711
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1712
|
+
* constructor(private url: string) {
|
|
1713
|
+
* super();
|
|
1714
|
+
* }
|
|
1715
|
+
* connect() { return `Connected to ${this.url}`; }
|
|
1716
|
+
* }
|
|
1717
|
+
*
|
|
1718
|
+
* class UserService extends Tag.Service('UserService') {
|
|
1719
|
+
* constructor(private db: DatabaseService, private timeout: number) {
|
|
1720
|
+
* super();
|
|
1721
|
+
* }
|
|
1722
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
1723
|
+
* }
|
|
1724
|
+
*
|
|
1725
|
+
* // Automatically inject DatabaseService and pass static timeout value
|
|
1726
|
+
* const userService = autoService(UserService, [DatabaseService, 5000]);
|
|
1727
|
+
* ```
|
|
1728
|
+
*
|
|
1729
|
+
* @example Mixed dependencies and static values
|
|
1730
|
+
* ```typescript
|
|
1731
|
+
* class NotificationService extends Tag.Service('NotificationService') {
|
|
1732
|
+
* constructor(
|
|
1733
|
+
* private logger: LoggerService,
|
|
1734
|
+
* private apiKey: string,
|
|
1735
|
+
* private retries: number,
|
|
1736
|
+
* private cache: CacheService
|
|
1737
|
+
* ) {
|
|
1738
|
+
* super();
|
|
1739
|
+
* }
|
|
1740
|
+
* }
|
|
1741
|
+
*
|
|
1742
|
+
* // Mix of DI tags and static values in constructor order
|
|
1743
|
+
* const notificationService = autoService(NotificationService, [
|
|
1744
|
+
* LoggerService, // Will be resolved from container
|
|
1745
|
+
* 'secret-api-key', // Static string value
|
|
1746
|
+
* 3, // Static number value
|
|
1747
|
+
* CacheService // Will be resolved from container
|
|
1748
|
+
* ]);
|
|
1749
|
+
* ```
|
|
1750
|
+
*
|
|
1751
|
+
* @example Compared to manual service creation
|
|
1752
|
+
* ```typescript
|
|
1753
|
+
* // Manual approach (more verbose)
|
|
1754
|
+
* const userServiceManual = service(UserService, async (ctx) => {
|
|
1755
|
+
* const db = await ctx.resolve(DatabaseService);
|
|
1756
|
+
* return new UserService(db, 5000);
|
|
1757
|
+
* });
|
|
1758
|
+
*
|
|
1759
|
+
* // Auto approach (concise)
|
|
1760
|
+
* const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
|
|
1761
|
+
* ```
|
|
1238
1762
|
*
|
|
1239
|
-
* @example
|
|
1763
|
+
* @example With finalizer for cleanup
|
|
1240
1764
|
* ```typescript
|
|
1241
|
-
* class DatabaseService extends Tag.
|
|
1242
|
-
* constructor(private
|
|
1765
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1766
|
+
* constructor(private connectionString: string) {
|
|
1243
1767
|
* super();
|
|
1244
1768
|
* }
|
|
1769
|
+
*
|
|
1770
|
+
* private connection: Connection | null = null;
|
|
1771
|
+
*
|
|
1772
|
+
* async connect() {
|
|
1773
|
+
* this.connection = await createConnection(this.connectionString);
|
|
1774
|
+
* }
|
|
1775
|
+
*
|
|
1776
|
+
* async disconnect() {
|
|
1777
|
+
* if (this.connection) {
|
|
1778
|
+
* await this.connection.close();
|
|
1779
|
+
* this.connection = null;
|
|
1780
|
+
* }
|
|
1781
|
+
* }
|
|
1245
1782
|
* }
|
|
1246
1783
|
*
|
|
1247
|
-
*
|
|
1784
|
+
* // Service with automatic cleanup
|
|
1785
|
+
* const dbService = autoService(
|
|
1248
1786
|
* DatabaseService,
|
|
1249
|
-
*
|
|
1787
|
+
* ['postgresql://localhost:5432/mydb'],
|
|
1788
|
+
* (service) => service.disconnect() // Finalizer for cleanup
|
|
1250
1789
|
* );
|
|
1251
1790
|
* ```
|
|
1252
1791
|
*/
|
|
1253
|
-
declare function service<T extends AnyTag, TParams = undefined>(serviceClass: T, factory: (container: IContainer<ServiceDependencies<T>>, params: TParams) => PromiseOrValue<ServiceOf<T>>): TParams extends undefined ? () => Service<T> : (params: TParams) => Service<T>;
|
|
1254
1792
|
//#endregion
|
|
1255
|
-
|
|
1793
|
+
//#region src/value.d.ts
|
|
1794
|
+
/**
|
|
1795
|
+
* Creates a layer that provides a constant value for a given tag.
|
|
1796
|
+
*
|
|
1797
|
+
* @param tag - The value tag to provide
|
|
1798
|
+
* @param constantValue - The constant value to provide
|
|
1799
|
+
* @returns A layer with no dependencies that provides the constant value
|
|
1800
|
+
*
|
|
1801
|
+
* @example
|
|
1802
|
+
* ```typescript
|
|
1803
|
+
* const ApiKey = Tag.of('ApiKey')<string>();
|
|
1804
|
+
* const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
|
|
1805
|
+
*
|
|
1806
|
+
* const apiKey = value(ApiKey, 'my-secret-key');
|
|
1807
|
+
* const dbUrl = value(DatabaseUrl, 'postgresql://localhost:5432/myapp');
|
|
1808
|
+
*
|
|
1809
|
+
* const config = Layer.merge(apiKey, dbUrl);
|
|
1810
|
+
* ```
|
|
1811
|
+
*/
|
|
1812
|
+
declare function value<Id extends TagId, T>(tag: ValueTag<Id, T>, constantValue: T): Layer<never, ValueTag<Id, T>>;
|
|
1813
|
+
//#endregion
|
|
1814
|
+
export { type AnyLayer, type AnyTag, CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, type DependencyLifecycle, type DependencySpec, type Factory, type Finalizer, type IContainer, type Inject, InjectSource, Layer, type PromiseOrValue, type ResolutionContext, type Scope, ScopedContainer, type ServiceTag, Tag, type TagType, UnknownDependencyError, type ValueTag, layer, scoped, service, value };
|