sandly 0.1.0 → 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 +371 -226
- package/dist/index.js +193 -136
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,50 +1,27 @@
|
|
|
1
1
|
//#region src/tag.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Type representing a tag identifier (string or symbol).
|
|
4
|
+
* @internal
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
type TagId = string | symbol;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* This
|
|
10
|
-
* the original tag information for dependency inference.
|
|
11
|
-
*
|
|
12
|
-
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
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.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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'.
|
|
16
13
|
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
20
|
-
*
|
|
21
|
-
* class UserService extends Tag.Class('UserService') {
|
|
22
|
-
* constructor(
|
|
23
|
-
* private db: DatabaseService, // ClassTag - works automatically
|
|
24
|
-
* private apiKey: Inject<typeof ApiKeyTag> // ValueTag - type is string, tag preserved
|
|
25
|
-
* ) {
|
|
26
|
-
* super();
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
type Inject<T extends ValueTag<unknown, string | symbol>> = T extends ValueTag<infer V, string | symbol> ? V & {
|
|
32
|
-
readonly [InjectSource]?: T;
|
|
33
|
-
} : never;
|
|
34
|
-
/**
|
|
35
|
-
* Helper type to extract the original ValueTag from an Inject<T> type.
|
|
36
|
-
* Since InjectSource is optional, we need to check for both presence and absence.
|
|
37
14
|
* @internal
|
|
38
15
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} ? U : never;
|
|
16
|
+
declare const ValueTagIdKey = "__sandly/ValueTagIdKey__";
|
|
17
|
+
declare const ServiceTagIdKey = "__sandly/ServiceTagIdKey__";
|
|
42
18
|
/**
|
|
43
|
-
* Internal symbol used to identify tagged
|
|
44
|
-
* This symbol is used as a property key to attach metadata to both value tags and
|
|
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.
|
|
45
22
|
* @internal
|
|
46
23
|
*/
|
|
47
|
-
declare const
|
|
24
|
+
declare const TagTypeKey: unique symbol;
|
|
48
25
|
/**
|
|
49
26
|
* Type representing a value-based dependency tag.
|
|
50
27
|
*
|
|
@@ -58,31 +35,30 @@ declare const TagId: "__tag_id__";
|
|
|
58
35
|
* @example
|
|
59
36
|
* ```typescript
|
|
60
37
|
* // Creates a value tag for string configuration
|
|
61
|
-
* const ApiKeyTag: ValueTag<
|
|
38
|
+
* const ApiKeyTag: ValueTag<'apiKey', string> = Tag.of('apiKey')<string>();
|
|
62
39
|
*
|
|
63
40
|
* // Register in container
|
|
64
41
|
* container.register(ApiKeyTag, () => 'my-secret-key');
|
|
65
42
|
* ```
|
|
66
43
|
*/
|
|
67
|
-
interface ValueTag<
|
|
68
|
-
readonly [
|
|
69
|
-
|
|
70
|
-
readonly __type: T;
|
|
44
|
+
interface ValueTag<Id extends TagId, T> {
|
|
45
|
+
readonly [ValueTagIdKey]: Id;
|
|
46
|
+
readonly [TagTypeKey]: T;
|
|
71
47
|
}
|
|
72
48
|
/**
|
|
73
49
|
* Type representing a class-based dependency tag.
|
|
74
50
|
*
|
|
75
|
-
* Tagged classes are created by Tag.
|
|
51
|
+
* Tagged classes are created by Tag.Service() and serve as both the dependency identifier
|
|
76
52
|
* and the constructor for the service. They extend regular classes with tag metadata
|
|
77
53
|
* that the DI system uses for identification and type safety.
|
|
78
54
|
*
|
|
79
|
-
* @template T - The type of instances created by this tagged class
|
|
80
55
|
* @template Id - The unique identifier for this tag (string or symbol)
|
|
56
|
+
* @template T - The type of instances created by this tagged class
|
|
81
57
|
*
|
|
82
58
|
* @example
|
|
83
59
|
* ```typescript
|
|
84
60
|
* // Creates a tagged class
|
|
85
|
-
* class UserService extends Tag.
|
|
61
|
+
* class UserService extends Tag.Service('UserService') {
|
|
86
62
|
* getUsers() { return []; }
|
|
87
63
|
* }
|
|
88
64
|
*
|
|
@@ -90,13 +66,13 @@ interface ValueTag<T, Id extends string | symbol> {
|
|
|
90
66
|
* container.register(UserService, () => new UserService());
|
|
91
67
|
* ```
|
|
92
68
|
*
|
|
93
|
-
* @internal - Users should use Tag.
|
|
69
|
+
* @internal - Users should use Tag.Service() instead of working with this type directly
|
|
94
70
|
*/
|
|
95
|
-
interface
|
|
71
|
+
interface ServiceTag<Id extends TagId, T> {
|
|
96
72
|
new (...args: any[]): T & {
|
|
97
|
-
readonly [
|
|
73
|
+
readonly [ServiceTagIdKey]: Id;
|
|
98
74
|
};
|
|
99
|
-
readonly [
|
|
75
|
+
readonly [ServiceTagIdKey]: Id;
|
|
100
76
|
}
|
|
101
77
|
/**
|
|
102
78
|
* Utility type that extracts the service type from any dependency tag.
|
|
@@ -105,7 +81,7 @@ interface ClassTag<T, Id extends string | symbol> {
|
|
|
105
81
|
* the container and layers to automatically determine what type of service
|
|
106
82
|
* a given tag represents without manual type annotations.
|
|
107
83
|
*
|
|
108
|
-
* @template T - Any dependency tag (ValueTag or
|
|
84
|
+
* @template T - Any dependency tag (ValueTag or ServiceTag)
|
|
109
85
|
* @returns The service type that the tag represents
|
|
110
86
|
*
|
|
111
87
|
* @example With value tags
|
|
@@ -117,9 +93,9 @@ interface ClassTag<T, Id extends string | symbol> {
|
|
|
117
93
|
* type ConfigService = TagType<typeof ConfigTag>; // { apiKey: string }
|
|
118
94
|
* ```
|
|
119
95
|
*
|
|
120
|
-
* @example With
|
|
96
|
+
* @example With service tags
|
|
121
97
|
* ```typescript
|
|
122
|
-
* class UserService extends Tag.
|
|
98
|
+
* class UserService extends Tag.Service('UserService') {
|
|
123
99
|
* getUsers() { return []; }
|
|
124
100
|
* }
|
|
125
101
|
*
|
|
@@ -132,11 +108,11 @@ interface ClassTag<T, Id extends string | symbol> {
|
|
|
132
108
|
* container.register(StringTag, () => 'hello'); // Factory must return string
|
|
133
109
|
* container.register(UserService, () => new UserService()); // Factory must return UserService
|
|
134
110
|
*
|
|
135
|
-
* const str: string = await container.
|
|
136
|
-
* const user: UserService = await container.
|
|
111
|
+
* const str: string = await container.resolve(StringTag); // Automatically typed as string
|
|
112
|
+
* const user: UserService = await container.resolve(UserService); // Automatically typed as UserService
|
|
137
113
|
* ```
|
|
138
114
|
*/
|
|
139
|
-
type TagType<
|
|
115
|
+
type TagType<TTag extends AnyTag> = TTag extends ValueTag<any, infer T> ? T : TTag extends ServiceTag<any, infer T> ? T : never;
|
|
140
116
|
/**
|
|
141
117
|
* Union type representing any valid dependency tag in the system.
|
|
142
118
|
*
|
|
@@ -152,15 +128,15 @@ type TagType<T> = T extends ValueTag<infer V, string | symbol> ? V : T extends C
|
|
|
152
128
|
*
|
|
153
129
|
* @example Class tag
|
|
154
130
|
* ```typescript
|
|
155
|
-
* class DatabaseService extends Tag.
|
|
131
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
156
132
|
* // DatabaseService satisfies AnyTag
|
|
157
133
|
* ```
|
|
158
134
|
*/
|
|
159
|
-
type AnyTag = ValueTag<
|
|
135
|
+
type AnyTag = ValueTag<TagId, any> | ServiceTag<TagId, any>;
|
|
160
136
|
/**
|
|
161
137
|
* Utility object containing factory functions for creating dependency tags.
|
|
162
138
|
*
|
|
163
|
-
* 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
|
|
164
140
|
* used throughout the dependency injection system. It's the main entry point for
|
|
165
141
|
* defining dependencies in a type-safe way.
|
|
166
142
|
*/
|
|
@@ -220,7 +196,7 @@ declare const Tag: {
|
|
|
220
196
|
* }));
|
|
221
197
|
* ```
|
|
222
198
|
*/
|
|
223
|
-
of: <Id extends
|
|
199
|
+
of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
|
|
224
200
|
/**
|
|
225
201
|
* Creates an anonymous value tag with a unique symbol identifier.
|
|
226
202
|
*
|
|
@@ -254,7 +230,7 @@ declare const Tag: {
|
|
|
254
230
|
* console.log(ConfigA === ConfigB); // false
|
|
255
231
|
* ```
|
|
256
232
|
*/
|
|
257
|
-
for: <T>() => ValueTag<
|
|
233
|
+
for: <T>() => ValueTag<symbol, T>;
|
|
258
234
|
/**
|
|
259
235
|
* Creates a base class that can be extended to create service classes with dependency tags.
|
|
260
236
|
*
|
|
@@ -268,7 +244,7 @@ declare const Tag: {
|
|
|
268
244
|
*
|
|
269
245
|
* @example Basic service class
|
|
270
246
|
* ```typescript
|
|
271
|
-
* class UserService extends Tag.
|
|
247
|
+
* class UserService extends Tag.Service('UserService') {
|
|
272
248
|
* getUsers() {
|
|
273
249
|
* return ['alice', 'bob'];
|
|
274
250
|
* }
|
|
@@ -279,11 +255,11 @@ declare const Tag: {
|
|
|
279
255
|
*
|
|
280
256
|
* @example Service with dependencies
|
|
281
257
|
* ```typescript
|
|
282
|
-
* class DatabaseService extends Tag.
|
|
258
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
283
259
|
* query(sql: string) { return []; }
|
|
284
260
|
* }
|
|
285
261
|
*
|
|
286
|
-
* class UserRepository extends Tag.
|
|
262
|
+
* class UserRepository extends Tag.Service('UserRepository') {
|
|
287
263
|
* constructor(private db: DatabaseService) {
|
|
288
264
|
* super();
|
|
289
265
|
* }
|
|
@@ -296,22 +272,22 @@ declare const Tag: {
|
|
|
296
272
|
* container
|
|
297
273
|
* .register(DatabaseService, () => new DatabaseService())
|
|
298
274
|
* .register(UserRepository, async (ctx) =>
|
|
299
|
-
* new UserRepository(await ctx.
|
|
275
|
+
* new UserRepository(await ctx.resolve(DatabaseService))
|
|
300
276
|
* );
|
|
301
277
|
* ```
|
|
302
278
|
*
|
|
303
279
|
* @example With symbol identifiers
|
|
304
280
|
* ```typescript
|
|
305
|
-
* const
|
|
281
|
+
* const ServiceId = Symbol('InternalService');
|
|
306
282
|
*
|
|
307
|
-
* class InternalService extends Tag.
|
|
283
|
+
* class InternalService extends Tag.Service(ServiceId) {
|
|
308
284
|
* doInternalWork() { return 'work'; }
|
|
309
285
|
* }
|
|
310
286
|
* ```
|
|
311
287
|
*/
|
|
312
|
-
|
|
313
|
-
readonly
|
|
314
|
-
}
|
|
288
|
+
Service: <Id extends TagId>(id: Id) => ServiceTag<Id, {
|
|
289
|
+
readonly "__sandly/ServiceTagIdKey__": Id;
|
|
290
|
+
}>;
|
|
315
291
|
/**
|
|
316
292
|
* Extracts the string representation of a tag's identifier.
|
|
317
293
|
*
|
|
@@ -319,14 +295,14 @@ declare const Tag: {
|
|
|
319
295
|
* whether it's a string-based or symbol-based tag. Primarily used internally
|
|
320
296
|
* for error messages and debugging.
|
|
321
297
|
*
|
|
322
|
-
* @param tag - Any valid dependency tag (value tag or
|
|
298
|
+
* @param tag - Any valid dependency tag (value tag or service tag)
|
|
323
299
|
* @returns String representation of the tag's identifier
|
|
324
300
|
*
|
|
325
301
|
* @example
|
|
326
302
|
* ```typescript
|
|
327
303
|
* const StringTag = Tag.of('myString')<string>();
|
|
328
304
|
* const SymbolTag = Tag.for<number>();
|
|
329
|
-
* class ServiceClass extends Tag.
|
|
305
|
+
* class ServiceClass extends Tag.Service('MyService') {}
|
|
330
306
|
*
|
|
331
307
|
* console.log(Tag.id(StringTag)); // "myString"
|
|
332
308
|
* console.log(Tag.id(SymbolTag)); // "Symbol()"
|
|
@@ -335,11 +311,54 @@ declare const Tag: {
|
|
|
335
311
|
*
|
|
336
312
|
* @internal - Primarily for internal use in error messages and debugging
|
|
337
313
|
*/
|
|
338
|
-
id: (tag: AnyTag) =>
|
|
314
|
+
id: (tag: AnyTag) => TagId;
|
|
315
|
+
isTag: (tag: unknown) => tag is AnyTag;
|
|
339
316
|
};
|
|
317
|
+
/**
|
|
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.
|
|
326
|
+
*
|
|
327
|
+
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
328
|
+
*
|
|
329
|
+
* @template T - A ValueTag type
|
|
330
|
+
* @returns The value type with optional phantom tag metadata for dependency inference
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
335
|
+
*
|
|
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
|
+
* }
|
|
343
|
+
* }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
type Inject<T extends ValueTag<TagId, unknown>> = T extends ValueTag<any, infer V> ? V & {
|
|
347
|
+
readonly [InjectSource]?: T;
|
|
348
|
+
} : never;
|
|
349
|
+
/**
|
|
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.
|
|
352
|
+
* @internal
|
|
353
|
+
*/
|
|
354
|
+
type ExtractInjectTag<T> = T extends {
|
|
355
|
+
readonly [InjectSource]?: infer U;
|
|
356
|
+
} ? U : never;
|
|
340
357
|
//#endregion
|
|
341
358
|
//#region src/types.d.ts
|
|
342
359
|
type PromiseOrValue<T> = T | Promise<T>;
|
|
360
|
+
type Contravariant<A> = (_: A) => void;
|
|
361
|
+
type Covariant<A> = (_: never) => A;
|
|
343
362
|
//#endregion
|
|
344
363
|
//#region src/container.d.ts
|
|
345
364
|
/**
|
|
@@ -366,8 +385,8 @@ type PromiseOrValue<T> = T | Promise<T>;
|
|
|
366
385
|
* ```typescript
|
|
367
386
|
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (ctx) => {
|
|
368
387
|
* const [config, db] = await Promise.all([
|
|
369
|
-
* ctx.
|
|
370
|
-
* ctx.
|
|
388
|
+
* ctx.resolve(ConfigTag),
|
|
389
|
+
* ctx.resolve(DatabaseService)
|
|
371
390
|
* ]);
|
|
372
391
|
* return new UserService(config, db);
|
|
373
392
|
* };
|
|
@@ -427,7 +446,7 @@ type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
|
427
446
|
*
|
|
428
447
|
* @example Using DependencyLifecycle for registration
|
|
429
448
|
* ```typescript
|
|
430
|
-
* class DatabaseConnection extends Tag.
|
|
449
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
431
450
|
* async connect() { return; }
|
|
432
451
|
* async disconnect() { return; }
|
|
433
452
|
* }
|
|
@@ -443,7 +462,7 @@ type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
|
443
462
|
* }
|
|
444
463
|
* };
|
|
445
464
|
*
|
|
446
|
-
*
|
|
465
|
+
* Container.empty().register(DatabaseConnection, lifecycle);
|
|
447
466
|
* ```
|
|
448
467
|
*/
|
|
449
468
|
type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
@@ -465,7 +484,7 @@ type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
|
465
484
|
* const spec: DependencySpec<typeof UserService, never> =
|
|
466
485
|
* () => new UserService();
|
|
467
486
|
*
|
|
468
|
-
*
|
|
487
|
+
* Container.empty().register(UserService, spec);
|
|
469
488
|
* ```
|
|
470
489
|
*
|
|
471
490
|
* @example Lifecycle registration
|
|
@@ -475,29 +494,34 @@ type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
|
475
494
|
* finalizer: (conn) => conn.close()
|
|
476
495
|
* };
|
|
477
496
|
*
|
|
478
|
-
*
|
|
497
|
+
* Container.empty().register(DatabaseConnection, spec);
|
|
479
498
|
* ```
|
|
480
499
|
*/
|
|
481
500
|
type DependencySpec<T extends AnyTag, TReg extends AnyTag> = Factory<TagType<T>, TReg> | DependencyLifecycle<TagType<T>, TReg>;
|
|
482
501
|
/**
|
|
483
502
|
* Type representing the context available to factory functions during dependency resolution.
|
|
484
503
|
*
|
|
485
|
-
* This type contains only the `
|
|
504
|
+
* This type contains only the `resolve` and `resolveAll` methods from the container, which are used to retrieve
|
|
486
505
|
* other dependencies during the creation of a service.
|
|
487
506
|
*
|
|
488
507
|
* @template TReg - Union type of all dependencies available in the container
|
|
489
508
|
*/
|
|
490
|
-
type ResolutionContext<TReg extends AnyTag> = Pick<IContainer<TReg>, '
|
|
509
|
+
type ResolutionContext<TReg extends AnyTag> = Pick<IContainer<TReg>, 'resolve' | 'resolveAll'>;
|
|
510
|
+
declare const ContainerTypeId: unique symbol;
|
|
491
511
|
/**
|
|
492
512
|
* Interface representing a container that can register and retrieve dependencies.
|
|
493
513
|
*
|
|
494
514
|
* @template TReg - Union type of all dependencies available in the container
|
|
495
515
|
*/
|
|
496
|
-
interface IContainer<
|
|
497
|
-
|
|
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>;
|
|
498
521
|
has(tag: AnyTag): boolean;
|
|
499
522
|
exists(tag: AnyTag): boolean;
|
|
500
|
-
|
|
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]> }>;
|
|
501
525
|
merge<TTarget extends AnyTag>(other: IContainer<TTarget>): IContainer<TReg | TTarget>;
|
|
502
526
|
destroy(): Promise<void>;
|
|
503
527
|
}
|
|
@@ -512,26 +536,26 @@ interface IContainer<in TReg extends AnyTag> {
|
|
|
512
536
|
*
|
|
513
537
|
* @template TReg - Union type of all registered dependency tags in this container
|
|
514
538
|
*
|
|
515
|
-
* @example Basic usage with
|
|
539
|
+
* @example Basic usage with service tags
|
|
516
540
|
* ```typescript
|
|
517
541
|
* import { container, Tag } from 'sandly';
|
|
518
542
|
*
|
|
519
|
-
* class DatabaseService extends Tag.
|
|
543
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
520
544
|
* query() { return 'data'; }
|
|
521
545
|
* }
|
|
522
546
|
*
|
|
523
|
-
* class UserService extends Tag.
|
|
547
|
+
* class UserService extends Tag.Service('UserService') {
|
|
524
548
|
* constructor(private db: DatabaseService) {}
|
|
525
549
|
* getUser() { return this.db.query(); }
|
|
526
550
|
* }
|
|
527
551
|
*
|
|
528
|
-
* const c =
|
|
552
|
+
* const c = Container.empty()
|
|
529
553
|
* .register(DatabaseService, () => new DatabaseService())
|
|
530
554
|
* .register(UserService, async (ctx) =>
|
|
531
|
-
* new UserService(await ctx.
|
|
555
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
532
556
|
* );
|
|
533
557
|
*
|
|
534
|
-
* const userService = await c.
|
|
558
|
+
* const userService = await c.resolve(UserService);
|
|
535
559
|
* ```
|
|
536
560
|
*
|
|
537
561
|
* @example Usage with value tags
|
|
@@ -539,22 +563,22 @@ interface IContainer<in TReg extends AnyTag> {
|
|
|
539
563
|
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
540
564
|
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
541
565
|
*
|
|
542
|
-
* const c =
|
|
566
|
+
* const c = Container.empty()
|
|
543
567
|
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
544
568
|
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
545
569
|
*
|
|
546
|
-
* const apiKey = await c.
|
|
547
|
-
* const config = await c.
|
|
570
|
+
* const apiKey = await c.resolve(ApiKeyTag);
|
|
571
|
+
* const config = await c.resolve(ConfigTag);
|
|
548
572
|
* ```
|
|
549
573
|
*
|
|
550
574
|
* @example With finalizers for cleanup
|
|
551
575
|
* ```typescript
|
|
552
|
-
* class DatabaseConnection extends Tag.
|
|
576
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
553
577
|
* async connect() { return; }
|
|
554
578
|
* async disconnect() { return; }
|
|
555
579
|
* }
|
|
556
580
|
*
|
|
557
|
-
* const c =
|
|
581
|
+
* const c = Container.empty().register(
|
|
558
582
|
* DatabaseConnection,
|
|
559
583
|
* async () => {
|
|
560
584
|
* const conn = new DatabaseConnection();
|
|
@@ -568,7 +592,10 @@ interface IContainer<in TReg extends AnyTag> {
|
|
|
568
592
|
* await c.destroy(); // Calls all finalizers
|
|
569
593
|
* ```
|
|
570
594
|
*/
|
|
571
|
-
declare class Container<
|
|
595
|
+
declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
596
|
+
readonly [ContainerTypeId]: {
|
|
597
|
+
readonly _TReg: Contravariant<TReg>;
|
|
598
|
+
};
|
|
572
599
|
/**
|
|
573
600
|
* Cache of instantiated dependencies as promises.
|
|
574
601
|
* Ensures singleton behavior and supports concurrent access.
|
|
@@ -590,12 +617,13 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
590
617
|
* @internal
|
|
591
618
|
*/
|
|
592
619
|
protected isDestroyed: boolean;
|
|
620
|
+
static empty(): Container<never>;
|
|
593
621
|
/**
|
|
594
622
|
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
595
623
|
*
|
|
596
624
|
* The factory function receives the current container instance and must return the
|
|
597
625
|
* service instance (or a Promise of it). The container tracks the registration at
|
|
598
|
-
* the type level, ensuring type safety for subsequent `.
|
|
626
|
+
* the type level, ensuring type safety for subsequent `.resolve()` calls.
|
|
599
627
|
*
|
|
600
628
|
* If a dependency is already registered, this method will override it unless the
|
|
601
629
|
* dependency has already been instantiated, in which case it will throw an error.
|
|
@@ -610,11 +638,11 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
610
638
|
*
|
|
611
639
|
* @example Registering a simple service
|
|
612
640
|
* ```typescript
|
|
613
|
-
* class LoggerService extends Tag.
|
|
641
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
614
642
|
* log(message: string) { console.log(message); }
|
|
615
643
|
* }
|
|
616
644
|
*
|
|
617
|
-
* const c =
|
|
645
|
+
* const c = Container.empty().register(
|
|
618
646
|
* LoggerService,
|
|
619
647
|
* () => new LoggerService()
|
|
620
648
|
* );
|
|
@@ -622,24 +650,24 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
622
650
|
*
|
|
623
651
|
* @example Registering with dependencies
|
|
624
652
|
* ```typescript
|
|
625
|
-
* class UserService extends Tag.
|
|
653
|
+
* class UserService extends Tag.Service('UserService') {
|
|
626
654
|
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
627
655
|
* }
|
|
628
656
|
*
|
|
629
|
-
* const c =
|
|
657
|
+
* const c = Container.empty()
|
|
630
658
|
* .register(DatabaseService, () => new DatabaseService())
|
|
631
659
|
* .register(LoggerService, () => new LoggerService())
|
|
632
660
|
* .register(UserService, async (ctx) =>
|
|
633
661
|
* new UserService(
|
|
634
|
-
* await ctx.
|
|
635
|
-
* await ctx.
|
|
662
|
+
* await ctx.resolve(DatabaseService),
|
|
663
|
+
* await ctx.resolve(LoggerService)
|
|
636
664
|
* )
|
|
637
665
|
* );
|
|
638
666
|
* ```
|
|
639
667
|
*
|
|
640
668
|
* @example Overriding a dependency
|
|
641
669
|
* ```typescript
|
|
642
|
-
* const c =
|
|
670
|
+
* const c = Container.empty()
|
|
643
671
|
* .register(DatabaseService, () => new DatabaseService())
|
|
644
672
|
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
645
673
|
* ```
|
|
@@ -648,7 +676,7 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
648
676
|
* ```typescript
|
|
649
677
|
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
650
678
|
*
|
|
651
|
-
* const c =
|
|
679
|
+
* const c = Container.empty().register(
|
|
652
680
|
* ConfigTag,
|
|
653
681
|
* () => ({ apiUrl: 'https://api.example.com' })
|
|
654
682
|
* );
|
|
@@ -656,12 +684,12 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
656
684
|
*
|
|
657
685
|
* @example With finalizer for cleanup
|
|
658
686
|
* ```typescript
|
|
659
|
-
* class DatabaseConnection extends Tag.
|
|
687
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
660
688
|
* async connect() { return; }
|
|
661
689
|
* async close() { return; }
|
|
662
690
|
* }
|
|
663
691
|
*
|
|
664
|
-
* const c =
|
|
692
|
+
* const c = Container.empty().register(
|
|
665
693
|
* DatabaseConnection,
|
|
666
694
|
* async () => {
|
|
667
695
|
* const conn = new DatabaseConnection();
|
|
@@ -684,7 +712,7 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
684
712
|
*
|
|
685
713
|
* @example
|
|
686
714
|
* ```typescript
|
|
687
|
-
* const c =
|
|
715
|
+
* const c = Container.empty().register(DatabaseService, () => new DatabaseService());
|
|
688
716
|
* console.log(c.has(DatabaseService)); // true
|
|
689
717
|
* ```
|
|
690
718
|
*/
|
|
@@ -715,10 +743,10 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
715
743
|
*
|
|
716
744
|
* @example Basic usage
|
|
717
745
|
* ```typescript
|
|
718
|
-
* const c =
|
|
746
|
+
* const c = Container.empty()
|
|
719
747
|
* .register(DatabaseService, () => new DatabaseService());
|
|
720
748
|
*
|
|
721
|
-
* const db = await c.
|
|
749
|
+
* const db = await c.resolve(DatabaseService);
|
|
722
750
|
* db.query('SELECT * FROM users');
|
|
723
751
|
* ```
|
|
724
752
|
*
|
|
@@ -726,9 +754,9 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
726
754
|
* ```typescript
|
|
727
755
|
* // All three calls will receive the same instance
|
|
728
756
|
* const [db1, db2, db3] = await Promise.all([
|
|
729
|
-
* c.
|
|
730
|
-
* c.
|
|
731
|
-
* c.
|
|
757
|
+
* c.resolve(DatabaseService),
|
|
758
|
+
* c.resolve(DatabaseService),
|
|
759
|
+
* c.resolve(DatabaseService)
|
|
732
760
|
* ]);
|
|
733
761
|
*
|
|
734
762
|
* console.log(db1 === db2 === db3); // true
|
|
@@ -736,17 +764,58 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
736
764
|
*
|
|
737
765
|
* @example Dependency injection in factories
|
|
738
766
|
* ```typescript
|
|
739
|
-
* const c =
|
|
767
|
+
* const c = Container.empty()
|
|
740
768
|
* .register(DatabaseService, () => new DatabaseService())
|
|
741
769
|
* .register(UserService, async (ctx) => {
|
|
742
|
-
* const db = await ctx.
|
|
770
|
+
* const db = await ctx.resolve(DatabaseService);
|
|
743
771
|
* return new UserService(db);
|
|
744
772
|
* });
|
|
745
773
|
*
|
|
746
|
-
* const userService = await c.
|
|
774
|
+
* const userService = await c.resolve(UserService);
|
|
747
775
|
* ```
|
|
748
776
|
*/
|
|
749
|
-
|
|
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]> }>;
|
|
750
819
|
/**
|
|
751
820
|
* Copies all registrations from this container to a target container.
|
|
752
821
|
*
|
|
@@ -771,10 +840,10 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
771
840
|
*
|
|
772
841
|
* @example Merging containers
|
|
773
842
|
* ```typescript
|
|
774
|
-
* const container1 =
|
|
843
|
+
* const container1 = Container.empty()
|
|
775
844
|
* .register(DatabaseService, () => new DatabaseService());
|
|
776
845
|
*
|
|
777
|
-
* const container2 =
|
|
846
|
+
* const container2 = Container.empty()
|
|
778
847
|
* .register(UserService, () => new UserService());
|
|
779
848
|
*
|
|
780
849
|
* const merged = container1.merge(container2);
|
|
@@ -803,7 +872,7 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
803
872
|
*
|
|
804
873
|
* @example Basic cleanup
|
|
805
874
|
* ```typescript
|
|
806
|
-
* const c =
|
|
875
|
+
* const c = Container.empty()
|
|
807
876
|
* .register(DatabaseConnection,
|
|
808
877
|
* async () => {
|
|
809
878
|
* const conn = new DatabaseConnection();
|
|
@@ -813,12 +882,12 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
813
882
|
* (conn) => conn.disconnect() // Finalizer
|
|
814
883
|
* );
|
|
815
884
|
*
|
|
816
|
-
* const db = await c.
|
|
885
|
+
* const db = await c.resolve(DatabaseConnection);
|
|
817
886
|
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
818
887
|
*
|
|
819
888
|
* // This will throw an error
|
|
820
889
|
* try {
|
|
821
|
-
* await c.
|
|
890
|
+
* await c.resolve(DatabaseConnection);
|
|
822
891
|
* } catch (error) {
|
|
823
892
|
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
824
893
|
* }
|
|
@@ -826,9 +895,9 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
826
895
|
*
|
|
827
896
|
* @example Application shutdown
|
|
828
897
|
* ```typescript
|
|
829
|
-
* const appContainer
|
|
898
|
+
* const appContainer Container.empty
|
|
830
899
|
* .register(DatabaseService, () => new DatabaseService())
|
|
831
|
-
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.
|
|
900
|
+
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
|
|
832
901
|
*
|
|
833
902
|
* // During application shutdown
|
|
834
903
|
* process.on('SIGTERM', async () => {
|
|
@@ -855,32 +924,6 @@ declare class Container<in TReg extends AnyTag = never> implements IContainer<TR
|
|
|
855
924
|
*/
|
|
856
925
|
destroy(): Promise<void>;
|
|
857
926
|
}
|
|
858
|
-
/**
|
|
859
|
-
* Creates a new empty dependency injection container.
|
|
860
|
-
*
|
|
861
|
-
* This is a convenience factory function that creates a new DependencyContainer instance.
|
|
862
|
-
* The returned container starts with no registered dependencies and the type parameter
|
|
863
|
-
* defaults to `never`, indicating no dependencies are available for retrieval yet.
|
|
864
|
-
*
|
|
865
|
-
* @returns A new empty DependencyContainer instance
|
|
866
|
-
*
|
|
867
|
-
* @example
|
|
868
|
-
* ```typescript
|
|
869
|
-
* import { container, Tag } from 'sandly';
|
|
870
|
-
*
|
|
871
|
-
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
872
|
-
* class UserService extends Tag.Class('UserService') {}
|
|
873
|
-
*
|
|
874
|
-
* const c = container()
|
|
875
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
876
|
-
* .register(UserService, async (ctx) =>
|
|
877
|
-
* new UserService(await ctx.get(DatabaseService))
|
|
878
|
-
* );
|
|
879
|
-
*
|
|
880
|
-
* const userService = await c.get(UserService);
|
|
881
|
-
* ```
|
|
882
|
-
*/
|
|
883
|
-
declare function container(): Container;
|
|
884
927
|
//#endregion
|
|
885
928
|
//#region src/errors.d.ts
|
|
886
929
|
type ErrorProps = {
|
|
@@ -917,7 +960,7 @@ declare class BaseError extends Error {
|
|
|
917
960
|
* @example Catching DI errors
|
|
918
961
|
* ```typescript
|
|
919
962
|
* try {
|
|
920
|
-
* await container.
|
|
963
|
+
* await container.resolve(SomeService);
|
|
921
964
|
* } catch (error) {
|
|
922
965
|
* if (error instanceof ContainerError) {
|
|
923
966
|
* console.error('DI Error:', error.message);
|
|
@@ -938,7 +981,7 @@ declare class DependencyAlreadyInstantiatedError extends ContainerError {}
|
|
|
938
981
|
/**
|
|
939
982
|
* Error thrown when attempting to use a container that has been destroyed.
|
|
940
983
|
*
|
|
941
|
-
* This error occurs when calling `container.
|
|
984
|
+
* This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
|
|
942
985
|
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
943
986
|
* is being used after it has been destroyed.
|
|
944
987
|
*/
|
|
@@ -946,16 +989,16 @@ declare class ContainerDestroyedError extends ContainerError {}
|
|
|
946
989
|
/**
|
|
947
990
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
948
991
|
*
|
|
949
|
-
* This error occurs when calling `container.
|
|
992
|
+
* This error occurs when calling `container.resolve(Tag)` for a tag that was never
|
|
950
993
|
* registered via `container.register()`. It indicates a programming error where
|
|
951
994
|
* the dependency setup is incomplete.
|
|
952
995
|
*
|
|
953
996
|
* @example
|
|
954
997
|
* ```typescript
|
|
955
|
-
* const c =
|
|
998
|
+
* const c = Container.empty(); // Empty container
|
|
956
999
|
*
|
|
957
1000
|
* try {
|
|
958
|
-
* await c.
|
|
1001
|
+
* await c.resolve(UnregisteredService); // This will throw
|
|
959
1002
|
* } catch (error) {
|
|
960
1003
|
* if (error instanceof UnknownDependencyError) {
|
|
961
1004
|
* console.error('Missing dependency:', error.message);
|
|
@@ -981,19 +1024,19 @@ declare class UnknownDependencyError extends ContainerError {
|
|
|
981
1024
|
*
|
|
982
1025
|
* @example Circular dependency scenario
|
|
983
1026
|
* ```typescript
|
|
984
|
-
* class ServiceA extends Tag.
|
|
985
|
-
* class ServiceB extends Tag.
|
|
1027
|
+
* class ServiceA extends Tag.Service('ServiceA') {}
|
|
1028
|
+
* class ServiceB extends Tag.Service('ServiceB') {}
|
|
986
1029
|
*
|
|
987
|
-
* const c =
|
|
1030
|
+
* const c = Container.empty()
|
|
988
1031
|
* .register(ServiceA, async (ctx) =>
|
|
989
|
-
* new ServiceA(await ctx.
|
|
1032
|
+
* new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
|
|
990
1033
|
* )
|
|
991
1034
|
* .register(ServiceB, async (ctx) =>
|
|
992
|
-
* new ServiceB(await ctx.
|
|
1035
|
+
* new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
|
|
993
1036
|
* );
|
|
994
1037
|
*
|
|
995
1038
|
* try {
|
|
996
|
-
* await c.
|
|
1039
|
+
* await c.resolve(ServiceA);
|
|
997
1040
|
* } catch (error) {
|
|
998
1041
|
* if (error instanceof CircularDependencyError) {
|
|
999
1042
|
* console.error('Circular dependency:', error.message);
|
|
@@ -1020,14 +1063,14 @@ declare class CircularDependencyError extends ContainerError {
|
|
|
1020
1063
|
*
|
|
1021
1064
|
* @example Factory throwing error
|
|
1022
1065
|
* ```typescript
|
|
1023
|
-
* class DatabaseService extends Tag.
|
|
1066
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
1024
1067
|
*
|
|
1025
|
-
* const c =
|
|
1068
|
+
* const c = Container.empty().register(DatabaseService, () => {
|
|
1026
1069
|
* throw new Error('Database connection failed');
|
|
1027
1070
|
* });
|
|
1028
1071
|
*
|
|
1029
1072
|
* try {
|
|
1030
|
-
* await c.
|
|
1073
|
+
* await c.resolve(DatabaseService);
|
|
1031
1074
|
* } catch (error) {
|
|
1032
1075
|
* if (error instanceof DependencyCreationError) {
|
|
1033
1076
|
* console.error('Failed to create:', error.message);
|
|
@@ -1077,18 +1120,13 @@ declare class DependencyFinalizationError extends ContainerError {
|
|
|
1077
1120
|
//#endregion
|
|
1078
1121
|
//#region src/layer.d.ts
|
|
1079
1122
|
/**
|
|
1080
|
-
* The most generic layer type that
|
|
1081
|
-
*
|
|
1082
|
-
* This type is carefully constructed to work with the Layer interface's variance annotations:
|
|
1083
|
-
* - `never` for TRequires (contravariant): Any layer requiring specific dependencies can be
|
|
1084
|
-
* assigned to this since requiring something is more restrictive than requiring nothing
|
|
1085
|
-
* - `AnyTag` for TProvides (covariant): Any layer providing specific services can be assigned
|
|
1086
|
-
* to this since the general AnyTag type can represent any specific tag type
|
|
1087
|
-
*
|
|
1088
|
-
* Used internally for functions like Layer.mergeAll() that need to accept arrays of layers
|
|
1089
|
-
* with different requirement/provision types while preserving type safety through variance.
|
|
1123
|
+
* The most generic layer type that accepts any concrete layer.
|
|
1090
1124
|
*/
|
|
1091
|
-
type AnyLayer = Layer<
|
|
1125
|
+
type AnyLayer = Layer<any, any>;
|
|
1126
|
+
/**
|
|
1127
|
+
* The type ID for the Layer interface.
|
|
1128
|
+
*/
|
|
1129
|
+
declare const LayerTypeId: unique symbol;
|
|
1092
1130
|
/**
|
|
1093
1131
|
* A dependency layer represents a reusable, composable unit of dependency registrations.
|
|
1094
1132
|
* Layers allow you to organize your dependency injection setup into logical groups
|
|
@@ -1098,12 +1136,12 @@ type AnyLayer = Layer<never, AnyTag>;
|
|
|
1098
1136
|
*
|
|
1099
1137
|
* The Layer interface uses TypeScript's variance annotations to enable safe substitutability:
|
|
1100
1138
|
*
|
|
1101
|
-
* ### TRequires (
|
|
1139
|
+
* ### TRequires (covariant)
|
|
1102
1140
|
* A layer requiring fewer dependencies can substitute one requiring more:
|
|
1103
1141
|
* - `Layer<never, X>` can be used where `Layer<A | B, X>` is expected
|
|
1104
1142
|
* - Intuition: A service that needs nothing is more flexible than one that needs specific deps
|
|
1105
1143
|
*
|
|
1106
|
-
* ### TProvides (
|
|
1144
|
+
* ### TProvides (contravariant)
|
|
1107
1145
|
* A layer providing more services can substitute one providing fewer:
|
|
1108
1146
|
* - `Layer<X, A | B>` can be used where `Layer<X, A>` is expected
|
|
1109
1147
|
* - Intuition: A service that gives you extra things is compatible with expecting fewer things
|
|
@@ -1115,7 +1153,7 @@ type AnyLayer = Layer<never, AnyTag>;
|
|
|
1115
1153
|
* ```typescript
|
|
1116
1154
|
* import { layer, Tag, container } from 'sandly';
|
|
1117
1155
|
*
|
|
1118
|
-
* class DatabaseService extends Tag.
|
|
1156
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1119
1157
|
* query() { return 'data'; }
|
|
1120
1158
|
* }
|
|
1121
1159
|
*
|
|
@@ -1125,10 +1163,10 @@ type AnyLayer = Layer<never, AnyTag>;
|
|
|
1125
1163
|
* );
|
|
1126
1164
|
*
|
|
1127
1165
|
* // Apply the layer to a container
|
|
1128
|
-
* const c =
|
|
1166
|
+
* const c = Container.empty();
|
|
1129
1167
|
* const finalContainer = databaseLayer.register(c);
|
|
1130
1168
|
*
|
|
1131
|
-
* const db = await finalContainer.
|
|
1169
|
+
* const db = await finalContainer.resolve(DatabaseService);
|
|
1132
1170
|
* ```
|
|
1133
1171
|
*
|
|
1134
1172
|
* @example Layer composition with variance
|
|
@@ -1136,18 +1174,19 @@ type AnyLayer = Layer<never, AnyTag>;
|
|
|
1136
1174
|
* // Layer that requires DatabaseService and provides UserService
|
|
1137
1175
|
* const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
|
|
1138
1176
|
* container.register(UserService, async (ctx) =>
|
|
1139
|
-
* new UserService(await ctx.
|
|
1177
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
1140
1178
|
* )
|
|
1141
1179
|
* );
|
|
1142
1180
|
*
|
|
1143
1181
|
* // Compose layers: provide database layer to user layer
|
|
1144
1182
|
* const appLayer = userLayer.provide(databaseLayer);
|
|
1145
|
-
*
|
|
1146
|
-
* // Thanks to variance, Layer<never, typeof DatabaseService> automatically works
|
|
1147
|
-
* // where Layer<typeof DatabaseService, typeof UserService> requires DatabaseService
|
|
1148
1183
|
* ```
|
|
1149
1184
|
*/
|
|
1150
|
-
interface Layer<
|
|
1185
|
+
interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
|
|
1186
|
+
readonly [LayerTypeId]?: {
|
|
1187
|
+
readonly _TRequires: Covariant<TRequires>;
|
|
1188
|
+
readonly _TProvides: Contravariant<TProvides>;
|
|
1189
|
+
};
|
|
1151
1190
|
/**
|
|
1152
1191
|
* Applies this layer's registrations to the given container.
|
|
1153
1192
|
*
|
|
@@ -1165,13 +1204,13 @@ interface Layer<in TRequires extends AnyTag = never, out TProvides extends AnyTa
|
|
|
1165
1204
|
*
|
|
1166
1205
|
* @example Basic usage
|
|
1167
1206
|
* ```typescript
|
|
1168
|
-
* const c =
|
|
1207
|
+
* const c = Container.empty();
|
|
1169
1208
|
* const updatedContainer = myLayer.register(c);
|
|
1170
1209
|
* ```
|
|
1171
1210
|
*
|
|
1172
1211
|
* @example With existing services preserved
|
|
1173
1212
|
* ```typescript
|
|
1174
|
-
* const baseContainer =
|
|
1213
|
+
* const baseContainer = Container.empty()
|
|
1175
1214
|
* .register(ExistingService, () => new ExistingService());
|
|
1176
1215
|
*
|
|
1177
1216
|
* const enhanced = myLayer.register(baseContainer);
|
|
@@ -1286,7 +1325,7 @@ interface Layer<in TRequires extends AnyTag = never, out TProvides extends AnyTa
|
|
|
1286
1325
|
* ```typescript
|
|
1287
1326
|
* import { layer, Tag } from 'sandly';
|
|
1288
1327
|
*
|
|
1289
|
-
* class DatabaseService extends Tag.
|
|
1328
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1290
1329
|
* constructor(private url: string = 'sqlite://memory') {}
|
|
1291
1330
|
* query() { return 'data'; }
|
|
1292
1331
|
* }
|
|
@@ -1311,15 +1350,15 @@ interface Layer<in TRequires extends AnyTag = never, out TProvides extends AnyTa
|
|
|
1311
1350
|
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
1312
1351
|
* (container) =>
|
|
1313
1352
|
* container
|
|
1314
|
-
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.
|
|
1315
|
-
* .register(CacheService, async (ctx) => new CacheService(await ctx.
|
|
1353
|
+
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
|
|
1354
|
+
* .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
|
|
1316
1355
|
* );
|
|
1317
1356
|
*
|
|
1318
1357
|
* // Service layer (requires infrastructure)
|
|
1319
1358
|
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
1320
1359
|
* (container) =>
|
|
1321
1360
|
* container.register(UserService, async (ctx) =>
|
|
1322
|
-
* new UserService(await ctx.
|
|
1361
|
+
* new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
|
|
1323
1362
|
* )
|
|
1324
1363
|
* );
|
|
1325
1364
|
*
|
|
@@ -1338,7 +1377,7 @@ declare function layer<TRequires extends AnyTag = never, TProvides extends AnyTa
|
|
|
1338
1377
|
*
|
|
1339
1378
|
* @internal
|
|
1340
1379
|
*/
|
|
1341
|
-
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R,
|
|
1380
|
+
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
|
|
1342
1381
|
/**
|
|
1343
1382
|
* Helper type that extracts the union of all provisions from an array of layers.
|
|
1344
1383
|
* Used by Layer.mergeAll() to compute the correct provision type for the merged layer.
|
|
@@ -1349,7 +1388,7 @@ type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] ext
|
|
|
1349
1388
|
*
|
|
1350
1389
|
* @internal
|
|
1351
1390
|
*/
|
|
1352
|
-
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<
|
|
1391
|
+
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
|
|
1353
1392
|
/**
|
|
1354
1393
|
* Utility object containing helper functions for working with layers.
|
|
1355
1394
|
*/
|
|
@@ -1370,7 +1409,7 @@ declare const Layer: {
|
|
|
1370
1409
|
* .merge(serviceLayer);
|
|
1371
1410
|
* ```
|
|
1372
1411
|
*/
|
|
1373
|
-
empty(): Layer
|
|
1412
|
+
empty(): Layer<never, never>;
|
|
1374
1413
|
/**
|
|
1375
1414
|
* Merges multiple layers at once in a type-safe way.
|
|
1376
1415
|
* This is equivalent to chaining `.merge()` calls but more convenient for multiple layers.
|
|
@@ -1461,11 +1500,12 @@ declare const Layer: {
|
|
|
1461
1500
|
//#endregion
|
|
1462
1501
|
//#region src/scoped-container.d.ts
|
|
1463
1502
|
type Scope = string | symbol;
|
|
1464
|
-
declare class ScopedContainer<
|
|
1503
|
+
declare class ScopedContainer<TReg extends AnyTag> extends Container<TReg> {
|
|
1465
1504
|
readonly scope: Scope;
|
|
1466
1505
|
private parent;
|
|
1467
1506
|
private readonly children;
|
|
1468
|
-
constructor(parent: IContainer<TReg> | null, scope: Scope);
|
|
1507
|
+
protected constructor(parent: IContainer<TReg> | null, scope: Scope);
|
|
1508
|
+
static empty(scope: Scope): ScopedContainer<never>;
|
|
1469
1509
|
/**
|
|
1470
1510
|
* Registers a dependency in the scoped container.
|
|
1471
1511
|
*
|
|
@@ -1496,7 +1536,7 @@ declare class ScopedContainer<in TReg extends AnyTag = never> extends Container<
|
|
|
1496
1536
|
* 3. Otherwise, delegate to parent scope
|
|
1497
1537
|
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
1498
1538
|
*/
|
|
1499
|
-
|
|
1539
|
+
resolve<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
1500
1540
|
/**
|
|
1501
1541
|
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
1502
1542
|
*
|
|
@@ -1548,7 +1588,7 @@ declare class ScopedContainer<in TReg extends AnyTag = never> extends Container<
|
|
|
1548
1588
|
* ```typescript
|
|
1549
1589
|
* import { container, scoped } from 'sandly';
|
|
1550
1590
|
*
|
|
1551
|
-
* const appContainer =
|
|
1591
|
+
* const appContainer = Container.empty()
|
|
1552
1592
|
* .register(DatabaseService, () => new DatabaseService())
|
|
1553
1593
|
* .register(ConfigService, () => new ConfigService());
|
|
1554
1594
|
*
|
|
@@ -1560,10 +1600,10 @@ declare class ScopedContainer<in TReg extends AnyTag = never> extends Container<
|
|
|
1560
1600
|
*
|
|
1561
1601
|
* @example Copying complex registrations
|
|
1562
1602
|
* ```typescript
|
|
1563
|
-
* const baseContainer =
|
|
1603
|
+
* const baseContainer = Container.empty()
|
|
1564
1604
|
* .register(DatabaseService, () => new DatabaseService())
|
|
1565
1605
|
* .register(UserService, {
|
|
1566
|
-
* factory: async (ctx) => new UserService(await ctx.
|
|
1606
|
+
* factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
|
|
1567
1607
|
* finalizer: (service) => service.cleanup()
|
|
1568
1608
|
* });
|
|
1569
1609
|
*
|
|
@@ -1575,49 +1615,58 @@ declare function scoped<TReg extends AnyTag>(container: Container<TReg>, scope:
|
|
|
1575
1615
|
//#endregion
|
|
1576
1616
|
//#region src/service.d.ts
|
|
1577
1617
|
/**
|
|
1578
|
-
* Extracts constructor parameter types from a
|
|
1618
|
+
* Extracts constructor parameter types from a ServiceTag.
|
|
1579
1619
|
* Only parameters that extend AnyTag are considered as dependencies.
|
|
1620
|
+
* @internal
|
|
1621
|
+
*/
|
|
1622
|
+
type ConstructorParams<T extends ServiceTag<TagId, unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1623
|
+
/**
|
|
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
|
|
1580
1631
|
*/
|
|
1581
|
-
type
|
|
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];
|
|
1582
1635
|
/**
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
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
|
|
1585
1640
|
*/
|
|
1586
|
-
|
|
1587
|
-
readonly [TagId]: infer Id;
|
|
1588
|
-
} ? Id extends string | symbol ? ClassTag<T, Id> : never : never;
|
|
1641
|
+
|
|
1589
1642
|
/**
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
* Handles both ClassTag dependencies (automatic) and ValueTag dependencies (via Inject helper).
|
|
1643
|
+
* Union of all dependency tags a ServiceTag constructor requires.
|
|
1644
|
+
* Filters out non‑DI parameters.
|
|
1593
1645
|
*/
|
|
1594
|
-
type
|
|
1595
|
-
readonly [TagId]: string | symbol;
|
|
1596
|
-
} ? InstanceToConstructorType<T[K]> : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
|
|
1646
|
+
type ServiceDependencies<T extends ServiceTag<TagId, unknown>> = ExtractConstructorDeps<ConstructorParams<T>> extends AnyTag ? ExtractConstructorDeps<ConstructorParams<T>> : never;
|
|
1597
1647
|
/**
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
* This is used to determine what dependencies a service requires.
|
|
1648
|
+
* Ordered tuple of dependency tags (and other constructor params)
|
|
1649
|
+
* inferred from a ServiceTag’s constructor.
|
|
1601
1650
|
*/
|
|
1602
|
-
|
|
1651
|
+
|
|
1603
1652
|
/**
|
|
1604
|
-
* Creates a service layer from any tag type (
|
|
1653
|
+
* Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
|
|
1605
1654
|
*
|
|
1606
|
-
* For
|
|
1655
|
+
* For ServiceTag services:
|
|
1607
1656
|
* - Dependencies are automatically inferred from constructor parameters
|
|
1608
1657
|
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
1609
1658
|
*
|
|
1610
1659
|
* For ValueTag services:
|
|
1611
1660
|
* - No constructor dependencies are needed since they don't have constructors
|
|
1612
1661
|
*
|
|
1613
|
-
* @template T - The tag representing the service (
|
|
1614
|
-
* @param
|
|
1662
|
+
* @template T - The tag representing the service (ServiceTag or ValueTag)
|
|
1663
|
+
* @param tag - The tag (ServiceTag or ValueTag)
|
|
1615
1664
|
* @param factory - Factory function for service instantiation with container
|
|
1616
1665
|
* @returns The service layer
|
|
1617
1666
|
*
|
|
1618
1667
|
* @example Simple service without dependencies
|
|
1619
1668
|
* ```typescript
|
|
1620
|
-
* class LoggerService extends Tag.
|
|
1669
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
1621
1670
|
* log(message: string) { console.log(message); }
|
|
1622
1671
|
* }
|
|
1623
1672
|
*
|
|
@@ -1626,11 +1675,11 @@ type ServiceDependencies<T extends AnyTag> = T extends ClassTag<unknown, string
|
|
|
1626
1675
|
*
|
|
1627
1676
|
* @example Service with dependencies
|
|
1628
1677
|
* ```typescript
|
|
1629
|
-
* class DatabaseService extends Tag.
|
|
1678
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1630
1679
|
* query() { return []; }
|
|
1631
1680
|
* }
|
|
1632
1681
|
*
|
|
1633
|
-
* class UserService extends Tag.
|
|
1682
|
+
* class UserService extends Tag.Service('UserService') {
|
|
1634
1683
|
* constructor(private db: DatabaseService) {
|
|
1635
1684
|
* super();
|
|
1636
1685
|
* }
|
|
@@ -1639,11 +1688,107 @@ type ServiceDependencies<T extends AnyTag> = T extends ClassTag<unknown, string
|
|
|
1639
1688
|
* }
|
|
1640
1689
|
*
|
|
1641
1690
|
* const userService = service(UserService, async (ctx) =>
|
|
1642
|
-
* new UserService(await ctx.
|
|
1691
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
1692
|
+
* );
|
|
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
|
+
* ```
|
|
1762
|
+
*
|
|
1763
|
+
* @example With finalizer for cleanup
|
|
1764
|
+
* ```typescript
|
|
1765
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1766
|
+
* constructor(private connectionString: string) {
|
|
1767
|
+
* super();
|
|
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
|
+
* }
|
|
1782
|
+
* }
|
|
1783
|
+
*
|
|
1784
|
+
* // Service with automatic cleanup
|
|
1785
|
+
* const dbService = autoService(
|
|
1786
|
+
* DatabaseService,
|
|
1787
|
+
* ['postgresql://localhost:5432/mydb'],
|
|
1788
|
+
* (service) => service.disconnect() // Finalizer for cleanup
|
|
1643
1789
|
* );
|
|
1644
1790
|
* ```
|
|
1645
1791
|
*/
|
|
1646
|
-
declare function service<T extends AnyTag>(serviceClass: T, spec: DependencySpec<T, ServiceDependencies<T>>): Layer<ServiceDependencies<T>, T>;
|
|
1647
1792
|
//#endregion
|
|
1648
1793
|
//#region src/value.d.ts
|
|
1649
1794
|
/**
|
|
@@ -1664,6 +1809,6 @@ declare function service<T extends AnyTag>(serviceClass: T, spec: DependencySpec
|
|
|
1664
1809
|
* const config = Layer.merge(apiKey, dbUrl);
|
|
1665
1810
|
* ```
|
|
1666
1811
|
*/
|
|
1667
|
-
declare function value<
|
|
1812
|
+
declare function value<Id extends TagId, T>(tag: ValueTag<Id, T>, constantValue: T): Layer<never, ValueTag<Id, T>>;
|
|
1668
1813
|
//#endregion
|
|
1669
|
-
export { type AnyLayer, type AnyTag, CircularDependencyError,
|
|
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 };
|