sandly 0.0.2 → 0.1.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/dist/index.d.ts +817 -403
- package/dist/index.js +426 -328
- package/package.json +75 -76
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
//#region src/tag.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Unique symbol used to store the original ValueTag in Inject<T> types.
|
|
4
|
+
* This prevents property name collisions while allowing type-level extraction.
|
|
5
|
+
*/
|
|
6
|
+
declare const InjectSource: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Helper type for injecting ValueTag dependencies in constructor parameters.
|
|
9
|
+
* This allows clean specification of ValueTag dependencies while preserving
|
|
10
|
+
* the original tag information for dependency inference.
|
|
11
|
+
*
|
|
12
|
+
* The phantom property is optional to allow normal runtime values to be assignable.
|
|
13
|
+
*
|
|
14
|
+
* @template T - A ValueTag type
|
|
15
|
+
* @returns The value type with optional phantom tag metadata for dependency inference
|
|
16
|
+
*
|
|
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
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
type ExtractInjectTag<T> = T extends {
|
|
40
|
+
readonly [InjectSource]?: infer U;
|
|
41
|
+
} ? U : never;
|
|
2
42
|
/**
|
|
3
43
|
* Internal symbol used to identify tagged types within the dependency injection system.
|
|
4
44
|
* This symbol is used as a property key to attach metadata to both value tags and class tags.
|
|
@@ -24,11 +64,11 @@ declare const TagId: "__tag_id__";
|
|
|
24
64
|
* container.register(ApiKeyTag, () => 'my-secret-key');
|
|
25
65
|
* ```
|
|
26
66
|
*/
|
|
27
|
-
|
|
67
|
+
interface ValueTag<T, Id extends string | symbol> {
|
|
28
68
|
readonly [TagId]: Id;
|
|
29
69
|
/** @internal Phantom type to carry T */
|
|
30
70
|
readonly __type: T;
|
|
31
|
-
}
|
|
71
|
+
}
|
|
32
72
|
/**
|
|
33
73
|
* Type representing a class-based dependency tag.
|
|
34
74
|
*
|
|
@@ -52,23 +92,51 @@ type ValueTag<T, Id extends string | symbol> = Readonly<{
|
|
|
52
92
|
*
|
|
53
93
|
* @internal - Users should use Tag.Class() instead of working with this type directly
|
|
54
94
|
*/
|
|
55
|
-
|
|
95
|
+
interface ClassTag<T, Id extends string | symbol> {
|
|
56
96
|
new (...args: any[]): T & {
|
|
57
97
|
readonly [TagId]: Id;
|
|
58
98
|
};
|
|
59
99
|
readonly [TagId]: Id;
|
|
60
|
-
}
|
|
100
|
+
}
|
|
61
101
|
/**
|
|
62
|
-
*
|
|
102
|
+
* Utility type that extracts the service type from any dependency tag.
|
|
103
|
+
*
|
|
104
|
+
* This type is essential for type inference throughout the DI system, allowing
|
|
105
|
+
* the container and layers to automatically determine what type of service
|
|
106
|
+
* a given tag represents without manual type annotations.
|
|
63
107
|
*
|
|
64
|
-
*
|
|
108
|
+
* @template T - Any dependency tag (ValueTag or ClassTag)
|
|
109
|
+
* @returns The service type that the tag represents
|
|
65
110
|
*
|
|
66
|
-
* @
|
|
67
|
-
*
|
|
111
|
+
* @example With value tags
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const StringTag = Tag.of('myString')<string>();
|
|
114
|
+
* const ConfigTag = Tag.of('config')<{ apiKey: string }>();
|
|
68
115
|
*
|
|
69
|
-
*
|
|
116
|
+
* type StringService = TagType<typeof StringTag>; // string
|
|
117
|
+
* type ConfigService = TagType<typeof ConfigTag>; // { apiKey: string }
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @example With class tags
|
|
121
|
+
* ```typescript
|
|
122
|
+
* class UserService extends Tag.Class('UserService') {
|
|
123
|
+
* getUsers() { return []; }
|
|
124
|
+
* }
|
|
125
|
+
*
|
|
126
|
+
* type UserServiceType = TagType<typeof UserService>; // UserService
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example Used in container methods
|
|
130
|
+
* ```typescript
|
|
131
|
+
* // The container uses TagType internally for type inference
|
|
132
|
+
* container.register(StringTag, () => 'hello'); // Factory must return string
|
|
133
|
+
* container.register(UserService, () => new UserService()); // Factory must return UserService
|
|
134
|
+
*
|
|
135
|
+
* const str: string = await container.get(StringTag); // Automatically typed as string
|
|
136
|
+
* const user: UserService = await container.get(UserService); // Automatically typed as UserService
|
|
137
|
+
* ```
|
|
70
138
|
*/
|
|
71
|
-
type
|
|
139
|
+
type TagType<T> = T extends ValueTag<infer V, string | symbol> ? V : T extends ClassTag<infer V, string | symbol> ? V : never;
|
|
72
140
|
/**
|
|
73
141
|
* Union type representing any valid dependency tag in the system.
|
|
74
142
|
*
|
|
@@ -88,7 +156,7 @@ type ClassTag<T> = TaggedClass<T, string | symbol>;
|
|
|
88
156
|
* // DatabaseService satisfies AnyTag
|
|
89
157
|
* ```
|
|
90
158
|
*/
|
|
91
|
-
type AnyTag = ValueTag<any, string | symbol> |
|
|
159
|
+
type AnyTag = ValueTag<any, string | symbol> | ClassTag<any, string | symbol>;
|
|
92
160
|
/**
|
|
93
161
|
* Utility object containing factory functions for creating dependency tags.
|
|
94
162
|
*
|
|
@@ -227,8 +295,8 @@ declare const Tag: {
|
|
|
227
295
|
*
|
|
228
296
|
* container
|
|
229
297
|
* .register(DatabaseService, () => new DatabaseService())
|
|
230
|
-
* .register(UserRepository, async (
|
|
231
|
-
* new UserRepository(await
|
|
298
|
+
* .register(UserRepository, async (ctx) =>
|
|
299
|
+
* new UserRepository(await ctx.get(DatabaseService))
|
|
232
300
|
* );
|
|
233
301
|
* ```
|
|
234
302
|
*
|
|
@@ -241,9 +309,7 @@ declare const Tag: {
|
|
|
241
309
|
* }
|
|
242
310
|
* ```
|
|
243
311
|
*/
|
|
244
|
-
Class: <Id extends string | symbol>(id: Id) =>
|
|
245
|
-
/** @internal */
|
|
246
|
-
readonly __type: unknown;
|
|
312
|
+
Class: <Id extends string | symbol>(id: Id) => ClassTag<{
|
|
247
313
|
readonly __tag_id__: Id;
|
|
248
314
|
}, Id>;
|
|
249
315
|
/**
|
|
@@ -271,63 +337,11 @@ declare const Tag: {
|
|
|
271
337
|
*/
|
|
272
338
|
id: (tag: AnyTag) => string;
|
|
273
339
|
};
|
|
274
|
-
/**
|
|
275
|
-
* Utility type that extracts the service type from any dependency tag.
|
|
276
|
-
*
|
|
277
|
-
* This type is essential for type inference throughout the DI system, allowing
|
|
278
|
-
* the container and layers to automatically determine what type of service
|
|
279
|
-
* a given tag represents without manual type annotations.
|
|
280
|
-
*
|
|
281
|
-
* @template T - Any dependency tag (ValueTag or TaggedClass)
|
|
282
|
-
* @returns The service type that the tag represents
|
|
283
|
-
*
|
|
284
|
-
* @example With value tags
|
|
285
|
-
* ```typescript
|
|
286
|
-
* const StringTag = Tag.of('myString')<string>();
|
|
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
|
-
* ```
|
|
292
|
-
*
|
|
293
|
-
* @example With class tags
|
|
294
|
-
* ```typescript
|
|
295
|
-
* class UserService extends Tag.Class('UserService') {
|
|
296
|
-
* getUsers() { return []; }
|
|
297
|
-
* }
|
|
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
|
-
* ```
|
|
311
|
-
*/
|
|
312
|
-
type ServiceOf<T> = T extends ValueTag<infer S, string | symbol> ? S : T extends ClassTag<infer S> ? S : never;
|
|
313
340
|
//#endregion
|
|
314
341
|
//#region src/types.d.ts
|
|
315
342
|
type PromiseOrValue<T> = T | Promise<T>;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
* This prevents property name collisions while allowing type-level extraction.
|
|
319
|
-
*/
|
|
320
|
-
declare const InjectSource: unique symbol;
|
|
321
|
-
/**
|
|
322
|
-
* Generic interface representing a class constructor.
|
|
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
|
|
328
|
-
* @internal
|
|
329
|
-
*/
|
|
330
|
-
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/container.d.ts
|
|
331
345
|
/**
|
|
332
346
|
* Type representing a factory function used to create dependency instances.
|
|
333
347
|
*
|
|
@@ -343,58 +357,23 @@ declare const InjectSource: unique symbol;
|
|
|
343
357
|
*
|
|
344
358
|
* @example Synchronous factory
|
|
345
359
|
* ```typescript
|
|
346
|
-
* const factory: Factory<DatabaseService, never> = (
|
|
360
|
+
* const factory: Factory<DatabaseService, never> = (ctx) => {
|
|
347
361
|
* return new DatabaseService('sqlite://memory');
|
|
348
362
|
* };
|
|
349
363
|
* ```
|
|
350
364
|
*
|
|
351
365
|
* @example Asynchronous factory with dependencies
|
|
352
366
|
* ```typescript
|
|
353
|
-
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (
|
|
367
|
+
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (ctx) => {
|
|
354
368
|
* const [config, db] = await Promise.all([
|
|
355
|
-
*
|
|
356
|
-
*
|
|
369
|
+
* ctx.get(ConfigTag),
|
|
370
|
+
* ctx.get(DatabaseService)
|
|
357
371
|
* ]);
|
|
358
372
|
* return new UserService(config, db);
|
|
359
373
|
* };
|
|
360
374
|
* ```
|
|
361
375
|
*/
|
|
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;
|
|
376
|
+
type Factory<T, TReg extends AnyTag> = (ctx: ResolutionContext<TReg>) => PromiseOrValue<T>;
|
|
398
377
|
/**
|
|
399
378
|
* Type representing a finalizer function used to clean up dependency instances.
|
|
400
379
|
*
|
|
@@ -436,19 +415,90 @@ type ExtractInjectTag<T> = T extends {
|
|
|
436
415
|
* ```
|
|
437
416
|
*/
|
|
438
417
|
type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Type representing a complete dependency lifecycle with both factory and finalizer.
|
|
420
|
+
*
|
|
421
|
+
* This type is used when registering dependencies that need cleanup. Instead of
|
|
422
|
+
* passing separate factory and finalizer parameters, you can pass an object
|
|
423
|
+
* containing both.
|
|
424
|
+
*
|
|
425
|
+
* @template T - The dependency tag type
|
|
426
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
427
|
+
*
|
|
428
|
+
* @example Using DependencyLifecycle for registration
|
|
429
|
+
* ```typescript
|
|
430
|
+
* class DatabaseConnection extends Tag.Class('DatabaseConnection') {
|
|
431
|
+
* async connect() { return; }
|
|
432
|
+
* async disconnect() { return; }
|
|
433
|
+
* }
|
|
434
|
+
*
|
|
435
|
+
* const lifecycle: DependencyLifecycle<typeof DatabaseConnection, never> = {
|
|
436
|
+
* factory: async () => {
|
|
437
|
+
* const conn = new DatabaseConnection();
|
|
438
|
+
* await conn.connect();
|
|
439
|
+
* return conn;
|
|
440
|
+
* },
|
|
441
|
+
* finalizer: async (conn) => {
|
|
442
|
+
* await conn.disconnect();
|
|
443
|
+
* }
|
|
444
|
+
* };
|
|
445
|
+
*
|
|
446
|
+
* container().register(DatabaseConnection, lifecycle);
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
type DependencyLifecycle<T, TReg extends AnyTag> = {
|
|
450
|
+
factory: Factory<T, TReg>;
|
|
451
|
+
finalizer: Finalizer<T>;
|
|
447
452
|
};
|
|
448
|
-
|
|
449
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Union type representing all valid dependency registration specifications.
|
|
455
|
+
*
|
|
456
|
+
* A dependency can be registered either as:
|
|
457
|
+
* - A simple factory function that creates the dependency
|
|
458
|
+
* - A complete lifecycle object with both factory and finalizer
|
|
459
|
+
*
|
|
460
|
+
* @template T - The dependency tag type
|
|
461
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
462
|
+
*
|
|
463
|
+
* @example Simple factory registration
|
|
464
|
+
* ```typescript
|
|
465
|
+
* const spec: DependencySpec<typeof UserService, never> =
|
|
466
|
+
* () => new UserService();
|
|
467
|
+
*
|
|
468
|
+
* container().register(UserService, spec);
|
|
469
|
+
* ```
|
|
470
|
+
*
|
|
471
|
+
* @example Lifecycle registration
|
|
472
|
+
* ```typescript
|
|
473
|
+
* const spec: DependencySpec<typeof DatabaseConnection, never> = {
|
|
474
|
+
* factory: () => new DatabaseConnection(),
|
|
475
|
+
* finalizer: (conn) => conn.close()
|
|
476
|
+
* };
|
|
477
|
+
*
|
|
478
|
+
* container().register(DatabaseConnection, spec);
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
type DependencySpec<T extends AnyTag, TReg extends AnyTag> = Factory<TagType<T>, TReg> | DependencyLifecycle<TagType<T>, TReg>;
|
|
482
|
+
/**
|
|
483
|
+
* Type representing the context available to factory functions during dependency resolution.
|
|
484
|
+
*
|
|
485
|
+
* This type contains only the `get` method from the container, which is used to retrieve
|
|
486
|
+
* other dependencies during the creation of a service.
|
|
487
|
+
*
|
|
488
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
489
|
+
*/
|
|
490
|
+
type ResolutionContext<TReg extends AnyTag> = Pick<IContainer<TReg>, 'get'>;
|
|
491
|
+
/**
|
|
492
|
+
* Interface representing a container that can register and retrieve dependencies.
|
|
493
|
+
*
|
|
494
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
495
|
+
*/
|
|
496
|
+
interface IContainer<in TReg extends AnyTag> {
|
|
497
|
+
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>): IContainer<TReg | T>;
|
|
450
498
|
has(tag: AnyTag): boolean;
|
|
451
|
-
|
|
499
|
+
exists(tag: AnyTag): boolean;
|
|
500
|
+
get<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
501
|
+
merge<TTarget extends AnyTag>(other: IContainer<TTarget>): IContainer<TReg | TTarget>;
|
|
452
502
|
destroy(): Promise<void>;
|
|
453
503
|
}
|
|
454
504
|
/**
|
|
@@ -464,7 +514,7 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
464
514
|
*
|
|
465
515
|
* @example Basic usage with class tags
|
|
466
516
|
* ```typescript
|
|
467
|
-
* import { container, Tag } from '
|
|
517
|
+
* import { container, Tag } from 'sandly';
|
|
468
518
|
*
|
|
469
519
|
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
470
520
|
* query() { return 'data'; }
|
|
@@ -477,8 +527,8 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
477
527
|
*
|
|
478
528
|
* const c = container()
|
|
479
529
|
* .register(DatabaseService, () => new DatabaseService())
|
|
480
|
-
* .register(UserService, async (
|
|
481
|
-
* new UserService(await
|
|
530
|
+
* .register(UserService, async (ctx) =>
|
|
531
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
482
532
|
* );
|
|
483
533
|
*
|
|
484
534
|
* const userService = await c.get(UserService);
|
|
@@ -518,23 +568,28 @@ interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
|
518
568
|
* await c.destroy(); // Calls all finalizers
|
|
519
569
|
* ```
|
|
520
570
|
*/
|
|
521
|
-
declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
571
|
+
declare class Container<in TReg extends AnyTag = never> implements IContainer<TReg> {
|
|
522
572
|
/**
|
|
523
573
|
* Cache of instantiated dependencies as promises.
|
|
524
574
|
* Ensures singleton behavior and supports concurrent access.
|
|
525
575
|
* @internal
|
|
526
576
|
*/
|
|
527
|
-
|
|
577
|
+
protected readonly cache: Map<AnyTag, Promise<unknown>>;
|
|
528
578
|
/**
|
|
529
579
|
* Factory functions for creating dependency instances.
|
|
530
580
|
* @internal
|
|
531
581
|
*/
|
|
532
|
-
|
|
582
|
+
protected readonly factories: Map<AnyTag, Factory<unknown, TReg>>;
|
|
533
583
|
/**
|
|
534
584
|
* Finalizer functions for cleaning up dependencies when the container is destroyed.
|
|
535
585
|
* @internal
|
|
536
586
|
*/
|
|
537
|
-
|
|
587
|
+
protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
|
|
588
|
+
/**
|
|
589
|
+
* Flag indicating whether this container has been destroyed.
|
|
590
|
+
* @internal
|
|
591
|
+
*/
|
|
592
|
+
protected isDestroyed: boolean;
|
|
538
593
|
/**
|
|
539
594
|
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
540
595
|
*
|
|
@@ -542,12 +597,16 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
542
597
|
* service instance (or a Promise of it). The container tracks the registration at
|
|
543
598
|
* the type level, ensuring type safety for subsequent `.get()` calls.
|
|
544
599
|
*
|
|
600
|
+
* If a dependency is already registered, this method will override it unless the
|
|
601
|
+
* dependency has already been instantiated, in which case it will throw an error.
|
|
602
|
+
*
|
|
545
603
|
* @template T - The dependency tag being registered
|
|
546
604
|
* @param tag - The dependency tag (class or value tag)
|
|
547
605
|
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
548
606
|
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
549
607
|
* @returns A new container instance with the dependency registered
|
|
550
|
-
* @throws {
|
|
608
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
609
|
+
* @throws {Error} If the dependency has already been instantiated
|
|
551
610
|
*
|
|
552
611
|
* @example Registering a simple service
|
|
553
612
|
* ```typescript
|
|
@@ -570,14 +629,21 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
570
629
|
* const c = container()
|
|
571
630
|
* .register(DatabaseService, () => new DatabaseService())
|
|
572
631
|
* .register(LoggerService, () => new LoggerService())
|
|
573
|
-
* .register(UserService, async (
|
|
632
|
+
* .register(UserService, async (ctx) =>
|
|
574
633
|
* new UserService(
|
|
575
|
-
* await
|
|
576
|
-
* await
|
|
634
|
+
* await ctx.get(DatabaseService),
|
|
635
|
+
* await ctx.get(LoggerService)
|
|
577
636
|
* )
|
|
578
637
|
* );
|
|
579
638
|
* ```
|
|
580
639
|
*
|
|
640
|
+
* @example Overriding a dependency
|
|
641
|
+
* ```typescript
|
|
642
|
+
* const c = container()
|
|
643
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
644
|
+
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
645
|
+
* ```
|
|
646
|
+
*
|
|
581
647
|
* @example Using value tags
|
|
582
648
|
* ```typescript
|
|
583
649
|
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
@@ -606,27 +672,30 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
606
672
|
* );
|
|
607
673
|
* ```
|
|
608
674
|
*/
|
|
609
|
-
register<T extends AnyTag>(tag: T,
|
|
675
|
+
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>): Container<TReg | T>;
|
|
610
676
|
/**
|
|
611
|
-
* Checks if a dependency has been
|
|
677
|
+
* Checks if a dependency has been registered in the container.
|
|
612
678
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
679
|
+
* This returns `true` if the dependency has been registered via `.register()`,
|
|
680
|
+
* regardless of whether it has been instantiated yet.
|
|
615
681
|
*
|
|
616
682
|
* @param tag - The dependency tag to check
|
|
617
|
-
* @returns `true` if the dependency has been
|
|
683
|
+
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
618
684
|
*
|
|
619
685
|
* @example
|
|
620
686
|
* ```typescript
|
|
621
687
|
* const c = container().register(DatabaseService, () => new DatabaseService());
|
|
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
|
|
688
|
+
* console.log(c.has(DatabaseService)); // true
|
|
627
689
|
* ```
|
|
628
690
|
*/
|
|
629
691
|
has(tag: AnyTag): boolean;
|
|
692
|
+
/**
|
|
693
|
+
* Checks if a dependency has been instantiated (cached) in the container.
|
|
694
|
+
*
|
|
695
|
+
* @param tag - The dependency tag to check
|
|
696
|
+
* @returns true if the dependency has been instantiated, false otherwise
|
|
697
|
+
*/
|
|
698
|
+
exists(tag: AnyTag): boolean;
|
|
630
699
|
/**
|
|
631
700
|
* Retrieves a dependency instance from the container, creating it if necessary.
|
|
632
701
|
*
|
|
@@ -669,21 +738,56 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
669
738
|
* ```typescript
|
|
670
739
|
* const c = container()
|
|
671
740
|
* .register(DatabaseService, () => new DatabaseService())
|
|
672
|
-
* .register(UserService, async (
|
|
673
|
-
* const db = await
|
|
741
|
+
* .register(UserService, async (ctx) => {
|
|
742
|
+
* const db = await ctx.get(DatabaseService);
|
|
674
743
|
* return new UserService(db);
|
|
675
744
|
* });
|
|
676
745
|
*
|
|
677
746
|
* const userService = await c.get(UserService);
|
|
678
747
|
* ```
|
|
679
748
|
*/
|
|
680
|
-
get<T extends TReg>(tag: T): Promise<
|
|
749
|
+
get<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
750
|
+
/**
|
|
751
|
+
* Copies all registrations from this container to a target container.
|
|
752
|
+
*
|
|
753
|
+
* @internal
|
|
754
|
+
* @param target - The container to copy registrations to
|
|
755
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
756
|
+
*/
|
|
757
|
+
copyTo<TTarget extends AnyTag>(target: Container<TTarget>): void;
|
|
758
|
+
/**
|
|
759
|
+
* Creates a new container by merging this container's registrations with another container.
|
|
760
|
+
*
|
|
761
|
+
* This method creates a new container that contains all registrations from both containers.
|
|
762
|
+
* If there are conflicts (same dependency registered in both containers), this
|
|
763
|
+
* container's registration will take precedence.
|
|
764
|
+
*
|
|
765
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
766
|
+
* The new container starts with an empty instance cache.
|
|
767
|
+
*
|
|
768
|
+
* @param other - The container to merge with
|
|
769
|
+
* @returns A new container with combined registrations
|
|
770
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
771
|
+
*
|
|
772
|
+
* @example Merging containers
|
|
773
|
+
* ```typescript
|
|
774
|
+
* const container1 = container()
|
|
775
|
+
* .register(DatabaseService, () => new DatabaseService());
|
|
776
|
+
*
|
|
777
|
+
* const container2 = container()
|
|
778
|
+
* .register(UserService, () => new UserService());
|
|
779
|
+
*
|
|
780
|
+
* const merged = container1.merge(container2);
|
|
781
|
+
* // merged has both DatabaseService and UserService
|
|
782
|
+
* ```
|
|
783
|
+
*/
|
|
784
|
+
merge<TTarget extends AnyTag>(other: Container<TTarget>): Container<TReg | TTarget>;
|
|
681
785
|
/**
|
|
682
|
-
* Destroys all instantiated dependencies by calling their finalizers
|
|
786
|
+
* Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
|
|
683
787
|
*
|
|
684
|
-
* **Important:
|
|
685
|
-
*
|
|
686
|
-
*
|
|
788
|
+
* **Important: After calling destroy(), the container becomes permanently unusable.**
|
|
789
|
+
* Any subsequent calls to register(), get(), or destroy() will throw a ContainerError.
|
|
790
|
+
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
687
791
|
*
|
|
688
792
|
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
689
793
|
* for maximum cleanup performance.
|
|
@@ -695,9 +799,9 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
695
799
|
* dependencies are cleaned up.
|
|
696
800
|
*
|
|
697
801
|
* @returns Promise that resolves when all cleanup is complete
|
|
698
|
-
* @throws {
|
|
802
|
+
* @throws {DependencyFinalizationError} If any finalizers fail during cleanup
|
|
699
803
|
*
|
|
700
|
-
* @example Basic cleanup
|
|
804
|
+
* @example Basic cleanup
|
|
701
805
|
* ```typescript
|
|
702
806
|
* const c = container()
|
|
703
807
|
* .register(DatabaseConnection,
|
|
@@ -709,24 +813,32 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
709
813
|
* (conn) => conn.disconnect() // Finalizer
|
|
710
814
|
* );
|
|
711
815
|
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* await c.destroy(); // Calls conn.disconnect(), clears cache
|
|
816
|
+
* const db = await c.get(DatabaseConnection);
|
|
817
|
+
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
715
818
|
*
|
|
716
|
-
* //
|
|
717
|
-
*
|
|
718
|
-
*
|
|
819
|
+
* // This will throw an error
|
|
820
|
+
* try {
|
|
821
|
+
* await c.get(DatabaseConnection);
|
|
822
|
+
* } catch (error) {
|
|
823
|
+
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
824
|
+
* }
|
|
719
825
|
* ```
|
|
720
826
|
*
|
|
721
|
-
* @example
|
|
827
|
+
* @example Application shutdown
|
|
722
828
|
* ```typescript
|
|
723
|
-
* const
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
829
|
+
* const appContainer = container()
|
|
830
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
831
|
+
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.get(DatabaseService)));
|
|
832
|
+
*
|
|
833
|
+
* // During application shutdown
|
|
834
|
+
* process.on('SIGTERM', async () => {
|
|
835
|
+
* try {
|
|
836
|
+
* await appContainer.destroy(); // Clean shutdown of all services
|
|
837
|
+
* } catch (error) {
|
|
838
|
+
* console.error('Error during shutdown:', error);
|
|
839
|
+
* }
|
|
840
|
+
* process.exit(0);
|
|
841
|
+
* });
|
|
730
842
|
* ```
|
|
731
843
|
*
|
|
732
844
|
* @example Handling cleanup errors
|
|
@@ -738,96 +850,11 @@ declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
|
738
850
|
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
739
851
|
* }
|
|
740
852
|
* }
|
|
741
|
-
* // Container is
|
|
853
|
+
* // Container is destroyed regardless of finalizer errors
|
|
742
854
|
* ```
|
|
743
855
|
*/
|
|
744
856
|
destroy(): Promise<void>;
|
|
745
857
|
}
|
|
746
|
-
declare class ScopedContainer<TReg extends AnyTag, TScope extends Scope> implements IContainer<TReg, TScope> {
|
|
747
|
-
private readonly scope;
|
|
748
|
-
private readonly parent;
|
|
749
|
-
private readonly children;
|
|
750
|
-
/**
|
|
751
|
-
* Cache of instantiated dependencies as promises for this scope.
|
|
752
|
-
* @internal
|
|
753
|
-
*/
|
|
754
|
-
private readonly cache;
|
|
755
|
-
/**
|
|
756
|
-
* Factory functions for creating dependency instances in this scope.
|
|
757
|
-
* @internal
|
|
758
|
-
*/
|
|
759
|
-
private readonly factories;
|
|
760
|
-
/**
|
|
761
|
-
* Finalizer functions for cleaning up dependencies when this scope is destroyed.
|
|
762
|
-
* @internal
|
|
763
|
-
*/
|
|
764
|
-
private readonly finalizers;
|
|
765
|
-
constructor(parent: IContainer<any, any> | null, scope: TScope);
|
|
766
|
-
/**
|
|
767
|
-
* Registers a dependency in the specified scope within this container's scope chain.
|
|
768
|
-
*
|
|
769
|
-
* If no scope is specified, registers in the current (leaf) scope. If a scope is specified,
|
|
770
|
-
* delegates to the parent container if the target scope doesn't match the current scope.
|
|
771
|
-
*
|
|
772
|
-
* This allows registering dependencies at different scope levels from any container
|
|
773
|
-
* in the scope chain, providing flexibility for dependency organization.
|
|
774
|
-
*
|
|
775
|
-
* @param tag - The dependency tag to register
|
|
776
|
-
* @param factory - Factory function to create the dependency
|
|
777
|
-
* @param finalizer - Optional cleanup function
|
|
778
|
-
* @param scope - Target scope for registration (defaults to current scope)
|
|
779
|
-
* @returns This container with updated type information
|
|
780
|
-
*
|
|
781
|
-
* @example Registering in different scopes
|
|
782
|
-
* ```typescript
|
|
783
|
-
* const runtime = scopedContainer('runtime');
|
|
784
|
-
* const request = runtime.child('request');
|
|
785
|
-
*
|
|
786
|
-
* // Register in current (request) scope
|
|
787
|
-
* request.register(RequestService, () => new RequestService());
|
|
788
|
-
*
|
|
789
|
-
* // Register in runtime scope from request container - delegates to parent
|
|
790
|
-
* request.register(DatabaseService, () => new DatabaseService(), undefined, 'runtime');
|
|
791
|
-
* ```
|
|
792
|
-
*/
|
|
793
|
-
register<T extends AnyTag>(tag: T, factoryOrLifecycle: Factory<ServiceOf<T>, TReg, TScope> | DependencyLifecycle<T, TReg, TScope>, scope?: TScope): ScopedContainer<TReg | T, TScope>;
|
|
794
|
-
/**
|
|
795
|
-
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
796
|
-
*
|
|
797
|
-
* This method checks the current scope first, then walks up the parent chain.
|
|
798
|
-
* Returns true only if the dependency has been created and cached somewhere in the scope hierarchy.
|
|
799
|
-
*/
|
|
800
|
-
has(tag: AnyTag): boolean;
|
|
801
|
-
/**
|
|
802
|
-
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
803
|
-
*
|
|
804
|
-
* Resolution strategy:
|
|
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
|
|
809
|
-
*/
|
|
810
|
-
get<T extends TReg>(tag: T): Promise<ServiceOf<T>>;
|
|
811
|
-
/**
|
|
812
|
-
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
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
|
|
818
|
-
*
|
|
819
|
-
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
820
|
-
* before their dependents.
|
|
821
|
-
*/
|
|
822
|
-
destroy(): Promise<void>;
|
|
823
|
-
/**
|
|
824
|
-
* Creates a child scoped container.
|
|
825
|
-
*
|
|
826
|
-
* Child containers inherit access to parent dependencies but maintain
|
|
827
|
-
* their own scope for new registrations and instance caching.
|
|
828
|
-
*/
|
|
829
|
-
child<TChildScope extends Scope>(scope: TChildScope): ScopedContainer<TReg, TScope | TChildScope>;
|
|
830
|
-
}
|
|
831
858
|
/**
|
|
832
859
|
* Creates a new empty dependency injection container.
|
|
833
860
|
*
|
|
@@ -839,35 +866,254 @@ declare class ScopedContainer<TReg extends AnyTag, TScope extends Scope> impleme
|
|
|
839
866
|
*
|
|
840
867
|
* @example
|
|
841
868
|
* ```typescript
|
|
842
|
-
* import { container, Tag } from '
|
|
869
|
+
* import { container, Tag } from 'sandly';
|
|
843
870
|
*
|
|
844
871
|
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
845
872
|
* class UserService extends Tag.Class('UserService') {}
|
|
846
873
|
*
|
|
847
874
|
* const c = container()
|
|
848
875
|
* .register(DatabaseService, () => new DatabaseService())
|
|
849
|
-
* .register(UserService, async (
|
|
850
|
-
* new UserService(await
|
|
876
|
+
* .register(UserService, async (ctx) =>
|
|
877
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
851
878
|
* );
|
|
852
879
|
*
|
|
853
880
|
* const userService = await c.get(UserService);
|
|
854
881
|
* ```
|
|
855
882
|
*/
|
|
856
|
-
declare function container(): Container
|
|
857
|
-
|
|
883
|
+
declare function container(): Container;
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/errors.d.ts
|
|
886
|
+
type ErrorProps = {
|
|
887
|
+
cause?: unknown;
|
|
888
|
+
detail?: Record<string, unknown>;
|
|
889
|
+
};
|
|
890
|
+
type ErrorDump = {
|
|
891
|
+
name: string;
|
|
892
|
+
message: string;
|
|
893
|
+
stack?: string;
|
|
894
|
+
error: {
|
|
895
|
+
name: string;
|
|
896
|
+
message: string;
|
|
897
|
+
detail: Record<string, unknown>;
|
|
898
|
+
cause?: unknown;
|
|
899
|
+
};
|
|
900
|
+
};
|
|
901
|
+
declare class BaseError extends Error {
|
|
902
|
+
detail: Record<string, unknown> | undefined;
|
|
903
|
+
constructor(message: string, {
|
|
904
|
+
cause,
|
|
905
|
+
detail
|
|
906
|
+
}?: ErrorProps);
|
|
907
|
+
static ensure(error: unknown): BaseError;
|
|
908
|
+
dump(): ErrorDump;
|
|
909
|
+
dumps(): string;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Base error class for all dependency container related errors.
|
|
913
|
+
*
|
|
914
|
+
* This extends the framework's BaseError to provide consistent error handling
|
|
915
|
+
* and structured error information across the dependency injection system.
|
|
916
|
+
*
|
|
917
|
+
* @example Catching DI errors
|
|
918
|
+
* ```typescript
|
|
919
|
+
* try {
|
|
920
|
+
* await container.get(SomeService);
|
|
921
|
+
* } catch (error) {
|
|
922
|
+
* if (error instanceof ContainerError) {
|
|
923
|
+
* console.error('DI Error:', error.message);
|
|
924
|
+
* console.error('Details:', error.detail);
|
|
925
|
+
* }
|
|
926
|
+
* }
|
|
927
|
+
* ```
|
|
928
|
+
*/
|
|
929
|
+
declare class ContainerError extends BaseError {}
|
|
930
|
+
/**
|
|
931
|
+
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
932
|
+
*
|
|
933
|
+
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
934
|
+
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
935
|
+
* by existing dependencies.
|
|
936
|
+
*/
|
|
937
|
+
declare class DependencyAlreadyInstantiatedError extends ContainerError {}
|
|
938
|
+
/**
|
|
939
|
+
* Error thrown when attempting to use a container that has been destroyed.
|
|
940
|
+
*
|
|
941
|
+
* This error occurs when calling `container.get()`, `container.register()`, or `container.destroy()`
|
|
942
|
+
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
943
|
+
* is being used after it has been destroyed.
|
|
944
|
+
*/
|
|
945
|
+
declare class ContainerDestroyedError extends ContainerError {}
|
|
946
|
+
/**
|
|
947
|
+
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
948
|
+
*
|
|
949
|
+
* This error occurs when calling `container.get(Tag)` for a tag that was never
|
|
950
|
+
* registered via `container.register()`. It indicates a programming error where
|
|
951
|
+
* the dependency setup is incomplete.
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const c = container(); // Empty container
|
|
956
|
+
*
|
|
957
|
+
* try {
|
|
958
|
+
* await c.get(UnregisteredService); // This will throw
|
|
959
|
+
* } catch (error) {
|
|
960
|
+
* if (error instanceof UnknownDependencyError) {
|
|
961
|
+
* console.error('Missing dependency:', error.message);
|
|
962
|
+
* }
|
|
963
|
+
* }
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
declare class UnknownDependencyError extends ContainerError {
|
|
967
|
+
/**
|
|
968
|
+
* @internal
|
|
969
|
+
* Creates an UnknownDependencyError for the given tag.
|
|
970
|
+
*
|
|
971
|
+
* @param tag - The dependency tag that wasn't found
|
|
972
|
+
*/
|
|
973
|
+
constructor(tag: AnyTag);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Error thrown when a circular dependency is detected during dependency resolution.
|
|
977
|
+
*
|
|
978
|
+
* This occurs when service A depends on service B, which depends on service A (directly
|
|
979
|
+
* or through a chain of dependencies). The error includes the full dependency chain
|
|
980
|
+
* to help identify the circular reference.
|
|
981
|
+
*
|
|
982
|
+
* @example Circular dependency scenario
|
|
983
|
+
* ```typescript
|
|
984
|
+
* class ServiceA extends Tag.Class('ServiceA') {}
|
|
985
|
+
* class ServiceB extends Tag.Class('ServiceB') {}
|
|
986
|
+
*
|
|
987
|
+
* const c = container()
|
|
988
|
+
* .register(ServiceA, async (ctx) =>
|
|
989
|
+
* new ServiceA(await ctx.get(ServiceB)) // Depends on B
|
|
990
|
+
* )
|
|
991
|
+
* .register(ServiceB, async (ctx) =>
|
|
992
|
+
* new ServiceB(await ctx.get(ServiceA)) // Depends on A - CIRCULAR!
|
|
993
|
+
* );
|
|
994
|
+
*
|
|
995
|
+
* try {
|
|
996
|
+
* await c.get(ServiceA);
|
|
997
|
+
* } catch (error) {
|
|
998
|
+
* if (error instanceof CircularDependencyError) {
|
|
999
|
+
* console.error('Circular dependency:', error.message);
|
|
1000
|
+
* // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
1001
|
+
* }
|
|
1002
|
+
* }
|
|
1003
|
+
* ```
|
|
1004
|
+
*/
|
|
1005
|
+
declare class CircularDependencyError extends ContainerError {
|
|
1006
|
+
/**
|
|
1007
|
+
* @internal
|
|
1008
|
+
* Creates a CircularDependencyError with the dependency chain information.
|
|
1009
|
+
*
|
|
1010
|
+
* @param tag - The tag where the circular dependency was detected
|
|
1011
|
+
* @param dependencyChain - The chain of dependencies that led to the circular reference
|
|
1012
|
+
*/
|
|
1013
|
+
constructor(tag: AnyTag, dependencyChain: AnyTag[]);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Error thrown when a dependency factory function throws an error during instantiation.
|
|
1017
|
+
*
|
|
1018
|
+
* This wraps the original error with additional context about which dependency
|
|
1019
|
+
* failed to be created. The original error is preserved as the `cause` property.
|
|
1020
|
+
*
|
|
1021
|
+
* @example Factory throwing error
|
|
1022
|
+
* ```typescript
|
|
1023
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
1024
|
+
*
|
|
1025
|
+
* const c = container().register(DatabaseService, () => {
|
|
1026
|
+
* throw new Error('Database connection failed');
|
|
1027
|
+
* });
|
|
1028
|
+
*
|
|
1029
|
+
* try {
|
|
1030
|
+
* await c.get(DatabaseService);
|
|
1031
|
+
* } catch (error) {
|
|
1032
|
+
* if (error instanceof DependencyCreationError) {
|
|
1033
|
+
* console.error('Failed to create:', error.message);
|
|
1034
|
+
* console.error('Original error:', error.cause);
|
|
1035
|
+
* }
|
|
1036
|
+
* }
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
declare class DependencyCreationError extends ContainerError {
|
|
1040
|
+
/**
|
|
1041
|
+
* @internal
|
|
1042
|
+
* Creates a DependencyCreationError wrapping the original factory error.
|
|
1043
|
+
*
|
|
1044
|
+
* @param tag - The tag of the dependency that failed to be created
|
|
1045
|
+
* @param error - The original error thrown by the factory function
|
|
1046
|
+
*/
|
|
1047
|
+
constructor(tag: AnyTag, error: unknown);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Error thrown when one or more finalizers fail during container destruction.
|
|
1051
|
+
*
|
|
1052
|
+
* This error aggregates multiple finalizer failures that occurred during
|
|
1053
|
+
* `container.destroy()`. Even if some finalizers fail, the container cleanup
|
|
1054
|
+
* process continues and this error contains details of all failures.
|
|
1055
|
+
*
|
|
1056
|
+
* @example Handling finalization errors
|
|
1057
|
+
* ```typescript
|
|
1058
|
+
* try {
|
|
1059
|
+
* await container.destroy();
|
|
1060
|
+
* } catch (error) {
|
|
1061
|
+
* if (error instanceof DependencyFinalizationError) {
|
|
1062
|
+
* console.error('Some finalizers failed');
|
|
1063
|
+
* console.error('Error details:', error.detail.errors);
|
|
1064
|
+
* }
|
|
1065
|
+
* }
|
|
1066
|
+
* ```
|
|
1067
|
+
*/
|
|
1068
|
+
declare class DependencyFinalizationError extends ContainerError {
|
|
1069
|
+
/**
|
|
1070
|
+
* @internal
|
|
1071
|
+
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
1072
|
+
*
|
|
1073
|
+
* @param errors - Array of errors thrown by individual finalizers
|
|
1074
|
+
*/
|
|
1075
|
+
constructor(errors: unknown[]);
|
|
1076
|
+
}
|
|
858
1077
|
//#endregion
|
|
859
1078
|
//#region src/layer.d.ts
|
|
1079
|
+
/**
|
|
1080
|
+
* The most generic layer type that works with variance - accepts any concrete layer.
|
|
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.
|
|
1090
|
+
*/
|
|
1091
|
+
type AnyLayer = Layer<never, AnyTag>;
|
|
860
1092
|
/**
|
|
861
1093
|
* A dependency layer represents a reusable, composable unit of dependency registrations.
|
|
862
1094
|
* Layers allow you to organize your dependency injection setup into logical groups
|
|
863
1095
|
* that can be combined and reused across different contexts.
|
|
864
1096
|
*
|
|
1097
|
+
* ## Type Variance
|
|
1098
|
+
*
|
|
1099
|
+
* The Layer interface uses TypeScript's variance annotations to enable safe substitutability:
|
|
1100
|
+
*
|
|
1101
|
+
* ### TRequires (contravariant with `in`)
|
|
1102
|
+
* A layer requiring fewer dependencies can substitute one requiring more:
|
|
1103
|
+
* - `Layer<never, X>` can be used where `Layer<A | B, X>` is expected
|
|
1104
|
+
* - Intuition: A service that needs nothing is more flexible than one that needs specific deps
|
|
1105
|
+
*
|
|
1106
|
+
* ### TProvides (covariant with `out`)
|
|
1107
|
+
* A layer providing more services can substitute one providing fewer:
|
|
1108
|
+
* - `Layer<X, A | B>` can be used where `Layer<X, A>` is expected
|
|
1109
|
+
* - Intuition: A service that gives you extra things is compatible with expecting fewer things
|
|
1110
|
+
*
|
|
865
1111
|
* @template TRequires - The union of tags this layer requires to be satisfied by other layers
|
|
866
1112
|
* @template TProvides - The union of tags this layer provides/registers
|
|
867
1113
|
*
|
|
868
1114
|
* @example Basic layer usage
|
|
869
1115
|
* ```typescript
|
|
870
|
-
* import { layer, Tag, container } from '
|
|
1116
|
+
* import { layer, Tag, container } from 'sandly';
|
|
871
1117
|
*
|
|
872
1118
|
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
873
1119
|
* query() { return 'data'; }
|
|
@@ -880,69 +1126,124 @@ declare function scopedContainer<TScope extends Scope>(scope: TScope): ScopedCon
|
|
|
880
1126
|
*
|
|
881
1127
|
* // Apply the layer to a container
|
|
882
1128
|
* const c = container();
|
|
883
|
-
* const finalContainer = databaseLayer
|
|
1129
|
+
* const finalContainer = databaseLayer.register(c);
|
|
884
1130
|
*
|
|
885
1131
|
* const db = await finalContainer.get(DatabaseService);
|
|
886
1132
|
* ```
|
|
887
1133
|
*
|
|
888
|
-
* @example Layer composition
|
|
1134
|
+
* @example Layer composition with variance
|
|
889
1135
|
* ```typescript
|
|
890
1136
|
* // Layer that requires DatabaseService and provides UserService
|
|
891
1137
|
* const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
|
|
892
|
-
* container.register(UserService, async (
|
|
893
|
-
* new UserService(await
|
|
1138
|
+
* container.register(UserService, async (ctx) =>
|
|
1139
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
894
1140
|
* )
|
|
895
1141
|
* );
|
|
896
1142
|
*
|
|
897
|
-
* // Compose layers: database layer
|
|
898
|
-
* const appLayer =
|
|
1143
|
+
* // Compose layers: provide database layer to user layer
|
|
1144
|
+
* 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
|
|
899
1148
|
* ```
|
|
900
1149
|
*/
|
|
901
|
-
interface Layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never> {
|
|
1150
|
+
interface Layer<in TRequires extends AnyTag = never, out TProvides extends AnyTag = never> {
|
|
902
1151
|
/**
|
|
903
1152
|
* Applies this layer's registrations to the given container.
|
|
904
1153
|
*
|
|
905
|
-
*
|
|
906
|
-
* @returns A new container with this layer's dependencies registered
|
|
1154
|
+
* ## Generic Container Support
|
|
907
1155
|
*
|
|
908
|
-
*
|
|
1156
|
+
* The signature uses `TContainer extends AnyTag` to accept containers with any existing
|
|
1157
|
+
* services while preserving type information. The container must provide at least this
|
|
1158
|
+
* layer's requirements (`TRequires`) but can have additional services (`TContainer`).
|
|
1159
|
+
*
|
|
1160
|
+
* Result container has: `TRequires | TContainer | TProvides` - everything that was
|
|
1161
|
+
* already there plus this layer's new provisions.
|
|
1162
|
+
*
|
|
1163
|
+
* @param container - The container to register dependencies into (must satisfy TRequires)
|
|
1164
|
+
* @returns A new container with this layer's dependencies registered and all existing services preserved
|
|
1165
|
+
*
|
|
1166
|
+
* @example Basic usage
|
|
909
1167
|
* ```typescript
|
|
910
|
-
* const
|
|
911
|
-
* const updatedContainer = myLayer.register(
|
|
1168
|
+
* const c = container();
|
|
1169
|
+
* const updatedContainer = myLayer.register(c);
|
|
1170
|
+
* ```
|
|
1171
|
+
*
|
|
1172
|
+
* @example With existing services preserved
|
|
1173
|
+
* ```typescript
|
|
1174
|
+
* const baseContainer = container()
|
|
1175
|
+
* .register(ExistingService, () => new ExistingService());
|
|
1176
|
+
*
|
|
1177
|
+
* const enhanced = myLayer.register(baseContainer);
|
|
1178
|
+
* // Enhanced container has both ExistingService and myLayer's provisions
|
|
912
1179
|
* ```
|
|
913
1180
|
*/
|
|
914
|
-
register: <
|
|
1181
|
+
register: <TContainer extends AnyTag>(container: IContainer<TRequires | TContainer>) => IContainer<TRequires | TContainer | TProvides>;
|
|
915
1182
|
/**
|
|
916
|
-
*
|
|
917
|
-
* provisions satisfy
|
|
918
|
-
* flow from source → target.
|
|
1183
|
+
* Provides a dependency layer to this layer, creating a pipeline where the dependency layer's
|
|
1184
|
+
* provisions satisfy this layer's requirements. This creates a dependency flow from dependency → this.
|
|
919
1185
|
*
|
|
920
|
-
* Type-safe:
|
|
1186
|
+
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
921
1187
|
* provisions and any remaining external requirements.
|
|
922
1188
|
*
|
|
923
|
-
* @template
|
|
924
|
-
* @template
|
|
925
|
-
* @param
|
|
926
|
-
* @returns A new composed layer
|
|
1189
|
+
* @template TDepRequires - What the dependency layer requires
|
|
1190
|
+
* @template TDepProvides - What the dependency layer provides
|
|
1191
|
+
* @param dependency - The layer to provide as a dependency
|
|
1192
|
+
* @returns A new composed layer that only exposes this layer's provisions
|
|
927
1193
|
*
|
|
928
1194
|
* @example Simple composition
|
|
929
1195
|
* ```typescript
|
|
930
1196
|
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
931
1197
|
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
932
1198
|
*
|
|
933
|
-
* //
|
|
934
|
-
* const infraLayer =
|
|
1199
|
+
* // Provide config to database layer
|
|
1200
|
+
* const infraLayer = dbLayer.provide(configLayer);
|
|
935
1201
|
* ```
|
|
936
1202
|
*
|
|
937
|
-
* @example Multi-level composition
|
|
1203
|
+
* @example Multi-level composition (reads naturally left-to-right)
|
|
938
1204
|
* ```typescript
|
|
939
|
-
* const appLayer =
|
|
940
|
-
* .
|
|
941
|
-
* .
|
|
942
|
-
* .
|
|
1205
|
+
* const appLayer = apiLayer
|
|
1206
|
+
* .provide(serviceLayer)
|
|
1207
|
+
* .provide(databaseLayer)
|
|
1208
|
+
* .provide(configLayer);
|
|
1209
|
+
* ```
|
|
1210
|
+
*/
|
|
1211
|
+
provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
|
|
1212
|
+
/**
|
|
1213
|
+
* Provides a dependency layer to this layer and merges the provisions.
|
|
1214
|
+
* Unlike `.provide()`, this method includes both this layer's provisions and the dependency layer's
|
|
1215
|
+
* provisions in the result type. This is useful when you want to expose services from both layers.
|
|
1216
|
+
*
|
|
1217
|
+
* Type-safe: This layer's requirements must be satisfiable by the dependency layer's
|
|
1218
|
+
* provisions and any remaining external requirements.
|
|
1219
|
+
*
|
|
1220
|
+
* @template TDepRequires - What the dependency layer requires
|
|
1221
|
+
* @template TDepProvides - What the dependency layer provides
|
|
1222
|
+
* @param dependency - The layer to provide as a dependency
|
|
1223
|
+
* @returns A new composed layer that provides services from both layers
|
|
1224
|
+
*
|
|
1225
|
+
* @example Providing with merged provisions
|
|
1226
|
+
* ```typescript
|
|
1227
|
+
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
1228
|
+
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
1229
|
+
*
|
|
1230
|
+
* // Provide config to database layer, and both services are available
|
|
1231
|
+
* const infraLayer = dbLayer.provideMerge(configLayer);
|
|
1232
|
+
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
1233
|
+
* ```
|
|
1234
|
+
*
|
|
1235
|
+
* @example Difference from .provide()
|
|
1236
|
+
* ```typescript
|
|
1237
|
+
* // .provide() only exposes this layer's provisions:
|
|
1238
|
+
* const withProvide = dbLayer.provide(configLayer);
|
|
1239
|
+
* // Type: Layer<never, typeof DatabaseService>
|
|
1240
|
+
*
|
|
1241
|
+
* // .provideMerge() exposes both layers' provisions:
|
|
1242
|
+
* const withProvideMerge = dbLayer.provideMerge(configLayer);
|
|
1243
|
+
* // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
|
|
943
1244
|
* ```
|
|
944
1245
|
*/
|
|
945
|
-
|
|
1246
|
+
provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
|
|
946
1247
|
/**
|
|
947
1248
|
* Merges this layer with another layer, combining their requirements and provisions.
|
|
948
1249
|
* This is useful for combining independent layers that don't have a dependency
|
|
@@ -959,40 +1260,31 @@ interface Layer<TRequires extends AnyTag = never, TProvides extends AnyTag = nev
|
|
|
959
1260
|
* const loggingLayer = layer<never, typeof LoggerService>(...);
|
|
960
1261
|
*
|
|
961
1262
|
* // Combine infrastructure layers
|
|
962
|
-
* const infraLayer = persistenceLayer
|
|
1263
|
+
* const infraLayer = persistenceLayer.merge(loggingLayer);
|
|
963
1264
|
* ```
|
|
964
1265
|
*
|
|
965
1266
|
* @example Building complex layer combinations
|
|
966
1267
|
* ```typescript
|
|
967
|
-
* const appInfraLayer = persistenceLayer
|
|
968
|
-
* .
|
|
969
|
-
* .
|
|
1268
|
+
* const appInfraLayer = persistenceLayer
|
|
1269
|
+
* .merge(messagingLayer)
|
|
1270
|
+
* .merge(observabilityLayer);
|
|
970
1271
|
* ```
|
|
971
1272
|
*/
|
|
972
|
-
|
|
1273
|
+
merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
|
|
973
1274
|
}
|
|
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
1275
|
/**
|
|
983
1276
|
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
984
1277
|
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
985
1278
|
*
|
|
986
1279
|
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
987
1280
|
* @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
1281
|
*
|
|
990
|
-
* @param register - Function that performs the dependency registrations. Receives a container
|
|
991
|
-
* @returns
|
|
1282
|
+
* @param register - Function that performs the dependency registrations. Receives a container.
|
|
1283
|
+
* @returns The layer instance.
|
|
992
1284
|
*
|
|
993
|
-
* @example Simple layer
|
|
1285
|
+
* @example Simple layer
|
|
994
1286
|
* ```typescript
|
|
995
|
-
* import { layer, Tag } from '
|
|
1287
|
+
* import { layer, Tag } from 'sandly';
|
|
996
1288
|
*
|
|
997
1289
|
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
998
1290
|
* constructor(private url: string = 'sqlite://memory') {}
|
|
@@ -1005,37 +1297,7 @@ type LayerFactory<TRequires extends AnyTag, TProvides extends AnyTag, TParams =
|
|
|
1005
1297
|
* );
|
|
1006
1298
|
*
|
|
1007
1299
|
* // 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 });
|
|
1300
|
+
* const dbLayerInstance = databaseLayer;
|
|
1039
1301
|
* ```
|
|
1040
1302
|
*
|
|
1041
1303
|
* @example Complex application layer structure
|
|
@@ -1049,37 +1311,45 @@ type LayerFactory<TRequires extends AnyTag, TProvides extends AnyTag, TParams =
|
|
|
1049
1311
|
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
1050
1312
|
* (container) =>
|
|
1051
1313
|
* container
|
|
1052
|
-
* .register(DatabaseService, async (
|
|
1053
|
-
* .register(CacheService, async (
|
|
1314
|
+
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.get(ConfigTag)))
|
|
1315
|
+
* .register(CacheService, async (ctx) => new CacheService(await ctx.get(ConfigTag)))
|
|
1054
1316
|
* );
|
|
1055
1317
|
*
|
|
1056
1318
|
* // Service layer (requires infrastructure)
|
|
1057
1319
|
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
1058
1320
|
* (container) =>
|
|
1059
|
-
* container.register(UserService, async (
|
|
1060
|
-
* new UserService(await
|
|
1321
|
+
* container.register(UserService, async (ctx) =>
|
|
1322
|
+
* new UserService(await ctx.get(DatabaseService), await ctx.get(CacheService))
|
|
1061
1323
|
* )
|
|
1062
1324
|
* );
|
|
1063
1325
|
*
|
|
1064
1326
|
* // Compose the complete application
|
|
1065
|
-
* const appLayer =
|
|
1327
|
+
* const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
|
|
1066
1328
|
* ```
|
|
1067
1329
|
*/
|
|
1068
|
-
declare function layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never
|
|
1330
|
+
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
1331
|
/**
|
|
1070
1332
|
* Helper type that extracts the union of all requirements from an array of layers.
|
|
1071
|
-
* Used by Layer.
|
|
1333
|
+
* Used by Layer.mergeAll() to compute the correct requirement type for the merged layer.
|
|
1334
|
+
*
|
|
1335
|
+
* Works with AnyLayer[] constraint which accepts any concrete layer through variance:
|
|
1336
|
+
* - Layer<never, X> → extracts `never` (no requirements)
|
|
1337
|
+
* - Layer<A | B, Y> → extracts `A | B` (specific requirements)
|
|
1072
1338
|
*
|
|
1073
1339
|
* @internal
|
|
1074
1340
|
*/
|
|
1075
|
-
type UnionOfRequires<T extends readonly
|
|
1341
|
+
type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, AnyTag> ? R : never }[number];
|
|
1076
1342
|
/**
|
|
1077
1343
|
* Helper type that extracts the union of all provisions from an array of layers.
|
|
1078
|
-
* Used by Layer.
|
|
1344
|
+
* Used by Layer.mergeAll() to compute the correct provision type for the merged layer.
|
|
1345
|
+
*
|
|
1346
|
+
* Works with AnyLayer[] constraint which accepts any concrete layer through variance:
|
|
1347
|
+
* - Layer<X, never> → extracts `never` (no provisions)
|
|
1348
|
+
* - Layer<Y, A | B> → extracts `A | B` (specific provisions)
|
|
1079
1349
|
*
|
|
1080
1350
|
* @internal
|
|
1081
1351
|
*/
|
|
1082
|
-
type UnionOfProvides<T extends readonly
|
|
1352
|
+
type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<never, infer P> ? P : never }[number];
|
|
1083
1353
|
/**
|
|
1084
1354
|
* Utility object containing helper functions for working with layers.
|
|
1085
1355
|
*/
|
|
@@ -1092,43 +1362,58 @@ declare const Layer: {
|
|
|
1092
1362
|
*
|
|
1093
1363
|
* @example
|
|
1094
1364
|
* ```typescript
|
|
1095
|
-
* import { Layer } from '
|
|
1365
|
+
* import { Layer } from 'sandly';
|
|
1096
1366
|
*
|
|
1097
1367
|
* const baseLayer = Layer.empty();
|
|
1098
1368
|
* const appLayer = baseLayer
|
|
1099
|
-
* .
|
|
1100
|
-
* .
|
|
1369
|
+
* .merge(configLayer)
|
|
1370
|
+
* .merge(serviceLayer);
|
|
1101
1371
|
* ```
|
|
1102
1372
|
*/
|
|
1103
1373
|
empty(): Layer;
|
|
1104
1374
|
/**
|
|
1105
1375
|
* Merges multiple layers at once in a type-safe way.
|
|
1106
|
-
* This is equivalent to chaining `.
|
|
1376
|
+
* This is equivalent to chaining `.merge()` calls but more convenient for multiple layers.
|
|
1377
|
+
*
|
|
1378
|
+
* ## Type Safety with Variance
|
|
1379
|
+
*
|
|
1380
|
+
* Uses the AnyLayer constraint (Layer<never, AnyTag>) which accepts any concrete layer
|
|
1381
|
+
* through the Layer interface's variance annotations:
|
|
1382
|
+
*
|
|
1383
|
+
* - **Contravariant TRequires**: Layer<typeof ServiceA, X> can be passed because requiring
|
|
1384
|
+
* ServiceA is more restrictive than requiring `never` (nothing)
|
|
1385
|
+
* - **Covariant TProvides**: Layer<Y, typeof ServiceB> can be passed because providing
|
|
1386
|
+
* ServiceB is compatible with the general `AnyTag` type
|
|
1387
|
+
*
|
|
1388
|
+
* The return type correctly extracts and unions the actual requirement/provision types
|
|
1389
|
+
* from all input layers, preserving full type safety.
|
|
1107
1390
|
*
|
|
1108
1391
|
* All layers are merged in order, combining their requirements and provisions.
|
|
1109
1392
|
* The resulting layer requires the union of all input layer requirements and
|
|
1110
1393
|
* provides the union of all input layer provisions.
|
|
1111
1394
|
*
|
|
1112
|
-
* @template T - The tuple type of layers to merge
|
|
1395
|
+
* @template T - The tuple type of layers to merge (constrained to AnyLayer for variance)
|
|
1113
1396
|
* @param layers - At least 2 layers to merge together
|
|
1114
|
-
* @returns A new layer that combines all input layers
|
|
1397
|
+
* @returns A new layer that combines all input layers with correct union types
|
|
1115
1398
|
*
|
|
1116
|
-
* @example Basic usage
|
|
1399
|
+
* @example Basic usage with different layer types
|
|
1117
1400
|
* ```typescript
|
|
1118
|
-
* import { Layer } from '
|
|
1401
|
+
* import { Layer } from 'sandly';
|
|
1119
1402
|
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1403
|
+
* // These all have different types but work thanks to variance:
|
|
1404
|
+
* const dbLayer = layer<never, typeof DatabaseService>(...); // no requirements
|
|
1405
|
+
* const userLayer = layer<typeof DatabaseService, typeof UserService>(...); // requires DB
|
|
1406
|
+
* const configLayer = layer<never, typeof ConfigService>(...); // no requirements
|
|
1407
|
+
*
|
|
1408
|
+
* const infraLayer = Layer.mergeAll(dbLayer, userLayer, configLayer);
|
|
1409
|
+
* // Type: Layer<typeof DatabaseService, typeof DatabaseService | typeof UserService | typeof ConfigService>
|
|
1125
1410
|
* ```
|
|
1126
1411
|
*
|
|
1127
|
-
* @example Equivalent to chaining .
|
|
1412
|
+
* @example Equivalent to chaining .merge()
|
|
1128
1413
|
* ```typescript
|
|
1129
1414
|
* // These are equivalent:
|
|
1130
|
-
* const layer1 = Layer.
|
|
1131
|
-
* const layer2 = layerA
|
|
1415
|
+
* const layer1 = Layer.mergeAll(layerA, layerB, layerC);
|
|
1416
|
+
* const layer2 = layerA.merge(layerB).merge(layerC);
|
|
1132
1417
|
* ```
|
|
1133
1418
|
*
|
|
1134
1419
|
* @example Building infrastructure layers
|
|
@@ -1138,31 +1423,169 @@ declare const Layer: {
|
|
|
1138
1423
|
* const observabilityLayer = layer<never, typeof Logger | typeof Metrics>(...);
|
|
1139
1424
|
*
|
|
1140
1425
|
* // Merge all infrastructure concerns into one layer
|
|
1141
|
-
* const infraLayer = Layer.
|
|
1142
|
-
* persistenceLayer
|
|
1143
|
-
* messagingLayer
|
|
1144
|
-
* observabilityLayer
|
|
1426
|
+
* const infraLayer = Layer.mergeAll(
|
|
1427
|
+
* persistenceLayer,
|
|
1428
|
+
* messagingLayer,
|
|
1429
|
+
* observabilityLayer
|
|
1145
1430
|
* );
|
|
1146
1431
|
*
|
|
1147
|
-
* //
|
|
1432
|
+
* // Result type: Layer<never, DatabaseService | CacheService | MessageQueue | Logger | Metrics>
|
|
1148
1433
|
* ```
|
|
1149
1434
|
*/
|
|
1150
|
-
|
|
1435
|
+
mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
1436
|
+
/**
|
|
1437
|
+
* Merges exactly two layers, combining their requirements and provisions.
|
|
1438
|
+
* This is similar to the `.merge()` method but available as a static function.
|
|
1439
|
+
*
|
|
1440
|
+
* @template TRequires1 - What the first layer requires
|
|
1441
|
+
* @template TProvides1 - What the first layer provides
|
|
1442
|
+
* @template TRequires2 - What the second layer requires
|
|
1443
|
+
* @template TProvides2 - What the second layer provides
|
|
1444
|
+
* @param layer1 - The first layer to merge
|
|
1445
|
+
* @param layer2 - The second layer to merge
|
|
1446
|
+
* @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
|
|
1447
|
+
*
|
|
1448
|
+
* @example Merging two layers
|
|
1449
|
+
* ```typescript
|
|
1450
|
+
* import { Layer } from 'sandly';
|
|
1451
|
+
*
|
|
1452
|
+
* const dbLayer = layer<never, typeof DatabaseService>(...);
|
|
1453
|
+
* const cacheLayer = layer<never, typeof CacheService>(...);
|
|
1454
|
+
*
|
|
1455
|
+
* const persistenceLayer = Layer.merge(dbLayer, cacheLayer);
|
|
1456
|
+
* // Type: Layer<never, typeof DatabaseService | typeof CacheService>
|
|
1457
|
+
* ```
|
|
1458
|
+
*/
|
|
1459
|
+
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
1460
|
};
|
|
1152
1461
|
//#endregion
|
|
1462
|
+
//#region src/scoped-container.d.ts
|
|
1463
|
+
type Scope = string | symbol;
|
|
1464
|
+
declare class ScopedContainer<in TReg extends AnyTag = never> extends Container<TReg> {
|
|
1465
|
+
readonly scope: Scope;
|
|
1466
|
+
private parent;
|
|
1467
|
+
private readonly children;
|
|
1468
|
+
constructor(parent: IContainer<TReg> | null, scope: Scope);
|
|
1469
|
+
/**
|
|
1470
|
+
* Registers a dependency in the scoped container.
|
|
1471
|
+
*
|
|
1472
|
+
* Overrides the base implementation to return ScopedContainer type
|
|
1473
|
+
* for proper method chaining support.
|
|
1474
|
+
*/
|
|
1475
|
+
register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TReg>): ScopedContainer<TReg | T>;
|
|
1476
|
+
/**
|
|
1477
|
+
* Checks if a dependency has been registered in this scope or any parent scope.
|
|
1478
|
+
*
|
|
1479
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
1480
|
+
* Returns true if the dependency has been registered somewhere in the scope hierarchy.
|
|
1481
|
+
*/
|
|
1482
|
+
has(tag: AnyTag): boolean;
|
|
1483
|
+
/**
|
|
1484
|
+
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
1485
|
+
*
|
|
1486
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
1487
|
+
* Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
|
|
1488
|
+
*/
|
|
1489
|
+
exists(tag: AnyTag): boolean;
|
|
1490
|
+
/**
|
|
1491
|
+
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
1492
|
+
*
|
|
1493
|
+
* Resolution strategy:
|
|
1494
|
+
* 1. Check cache in current scope
|
|
1495
|
+
* 2. Check if factory exists in current scope - if so, create instance here
|
|
1496
|
+
* 3. Otherwise, delegate to parent scope
|
|
1497
|
+
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
1498
|
+
*/
|
|
1499
|
+
get<T extends TReg>(tag: T): Promise<TagType<T>>;
|
|
1500
|
+
/**
|
|
1501
|
+
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
1502
|
+
*
|
|
1503
|
+
* This method ensures proper cleanup order while maintaining reusability:
|
|
1504
|
+
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
1505
|
+
* 2. Then calls finalizers for dependencies created in this scope
|
|
1506
|
+
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
1507
|
+
*
|
|
1508
|
+
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
1509
|
+
* before their dependents.
|
|
1510
|
+
*/
|
|
1511
|
+
destroy(): Promise<void>;
|
|
1512
|
+
/**
|
|
1513
|
+
* Creates a new scoped container by merging this container's registrations with another container.
|
|
1514
|
+
*
|
|
1515
|
+
* This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
|
|
1516
|
+
* The resulting scoped container contains all registrations from both containers and becomes a root scope
|
|
1517
|
+
* (no parent) with the scope name from this container.
|
|
1518
|
+
*
|
|
1519
|
+
* @param other - The container to merge with
|
|
1520
|
+
* @returns A new ScopedContainer with combined registrations
|
|
1521
|
+
* @throws {ContainerDestroyedError} If this container has been destroyed
|
|
1522
|
+
*/
|
|
1523
|
+
merge<TTarget extends AnyTag>(other: Container<TTarget>): ScopedContainer<TReg | TTarget>;
|
|
1524
|
+
/**
|
|
1525
|
+
* Creates a child scoped container.
|
|
1526
|
+
*
|
|
1527
|
+
* Child containers inherit access to parent dependencies but maintain
|
|
1528
|
+
* their own scope for new registrations and instance caching.
|
|
1529
|
+
*/
|
|
1530
|
+
child(scope: Scope): ScopedContainer<TReg>;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Converts a regular container into a scoped container, copying all registrations.
|
|
1534
|
+
*
|
|
1535
|
+
* This function creates a new ScopedContainer instance and copies all factory functions
|
|
1536
|
+
* and finalizers from the source container. The resulting scoped container becomes a root
|
|
1537
|
+
* scope (no parent) with all the same dependency registrations.
|
|
1538
|
+
*
|
|
1539
|
+
* **Important**: Only the registrations are copied, not any cached instances.
|
|
1540
|
+
* The new scoped container starts with an empty instance cache.
|
|
1541
|
+
*
|
|
1542
|
+
* @param container - The container to convert to a scoped container
|
|
1543
|
+
* @param scope - A string or symbol identifier for this scope (used for debugging)
|
|
1544
|
+
* @returns A new ScopedContainer instance with all registrations copied from the source container
|
|
1545
|
+
* @throws {ContainerDestroyedError} If the source container has been destroyed
|
|
1546
|
+
*
|
|
1547
|
+
* @example Converting a regular container to scoped
|
|
1548
|
+
* ```typescript
|
|
1549
|
+
* import { container, scoped } from 'sandly';
|
|
1550
|
+
*
|
|
1551
|
+
* const appContainer = container()
|
|
1552
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
1553
|
+
* .register(ConfigService, () => new ConfigService());
|
|
1554
|
+
*
|
|
1555
|
+
* const scopedAppContainer = scoped(appContainer, 'app');
|
|
1556
|
+
*
|
|
1557
|
+
* // Create child scopes
|
|
1558
|
+
* const requestContainer = scopedAppContainer.child('request');
|
|
1559
|
+
* ```
|
|
1560
|
+
*
|
|
1561
|
+
* @example Copying complex registrations
|
|
1562
|
+
* ```typescript
|
|
1563
|
+
* const baseContainer = container()
|
|
1564
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
1565
|
+
* .register(UserService, {
|
|
1566
|
+
* factory: async (ctx) => new UserService(await ctx.get(DatabaseService)),
|
|
1567
|
+
* finalizer: (service) => service.cleanup()
|
|
1568
|
+
* });
|
|
1569
|
+
*
|
|
1570
|
+
* const scopedContainer = scoped(baseContainer, 'app');
|
|
1571
|
+
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
1572
|
+
* ```
|
|
1573
|
+
*/
|
|
1574
|
+
declare function scoped<TReg extends AnyTag>(container: Container<TReg>, scope: Scope): ScopedContainer<TReg>;
|
|
1575
|
+
//#endregion
|
|
1153
1576
|
//#region src/service.d.ts
|
|
1154
1577
|
/**
|
|
1155
|
-
* Extracts constructor parameter types from a
|
|
1578
|
+
* Extracts constructor parameter types from a ClassTag.
|
|
1156
1579
|
* Only parameters that extend AnyTag are considered as dependencies.
|
|
1157
1580
|
*/
|
|
1158
|
-
type ConstructorParams<T extends ClassTag<unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1581
|
+
type ConstructorParams<T extends ClassTag<unknown, string | symbol>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1159
1582
|
/**
|
|
1160
1583
|
* Helper to convert a tagged instance type back to its constructor type.
|
|
1161
1584
|
* This uses the fact that tagged classes have a specific structure with TagId property.
|
|
1162
1585
|
*/
|
|
1163
1586
|
type InstanceToConstructorType<T> = T extends {
|
|
1164
1587
|
readonly [TagId]: infer Id;
|
|
1165
|
-
} ? Id extends string | symbol ?
|
|
1588
|
+
} ? Id extends string | symbol ? ClassTag<T, Id> : never : never;
|
|
1166
1589
|
/**
|
|
1167
1590
|
* Extracts constructor-typed dependencies from constructor parameters.
|
|
1168
1591
|
* Converts instance types to their corresponding constructor types.
|
|
@@ -1171,27 +1594,12 @@ type InstanceToConstructorType<T> = T extends {
|
|
|
1171
1594
|
type FilterTags<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1172
1595
|
readonly [TagId]: string | symbol;
|
|
1173
1596
|
} ? InstanceToConstructorType<T[K]> : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
|
|
1174
|
-
/**
|
|
1175
|
-
* Extracts the instance type that a TaggedClass constructor creates.
|
|
1176
|
-
*/
|
|
1177
|
-
|
|
1178
1597
|
/**
|
|
1179
1598
|
* Extracts only the dependency tags from a constructor's parameters for ClassTag services,
|
|
1180
1599
|
* or returns never for ValueTag services (which have no constructor dependencies).
|
|
1181
1600
|
* This is used to determine what dependencies a service requires.
|
|
1182
1601
|
*/
|
|
1183
|
-
type ServiceDependencies<T extends AnyTag> = T extends ClassTag<unknown> ? FilterTags<ConstructorParams<T>> extends AnyTag ? FilterTags<ConstructorParams<T>> : never : never;
|
|
1184
|
-
/**
|
|
1185
|
-
* Represents a service layer that can be created from any tag type.
|
|
1186
|
-
* For ClassTag services, dependencies are automatically inferred from constructor parameters.
|
|
1187
|
-
* For ValueTag services, there are no dependencies since they don't have constructors.
|
|
1188
|
-
*/
|
|
1189
|
-
interface Service<T extends AnyTag> extends Layer<ServiceDependencies<T>, T> {
|
|
1190
|
-
/**
|
|
1191
|
-
* The tag that this service represents (ClassTag or ValueTag)
|
|
1192
|
-
*/
|
|
1193
|
-
readonly serviceClass: T;
|
|
1194
|
-
}
|
|
1602
|
+
type ServiceDependencies<T extends AnyTag> = T extends ClassTag<unknown, string | symbol> ? FilterTags<ConstructorParams<T>> extends AnyTag ? FilterTags<ConstructorParams<T>> : never : never;
|
|
1195
1603
|
/**
|
|
1196
1604
|
* Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
|
|
1197
1605
|
*
|
|
@@ -1203,10 +1611,9 @@ interface Service<T extends AnyTag> extends Layer<ServiceDependencies<T>, T> {
|
|
|
1203
1611
|
* - No constructor dependencies are needed since they don't have constructors
|
|
1204
1612
|
*
|
|
1205
1613
|
* @template T - The tag representing the service (ClassTag or ValueTag)
|
|
1206
|
-
* @template TParams - Optional parameters for service configuration
|
|
1207
1614
|
* @param serviceClass - The tag (ClassTag or ValueTag)
|
|
1208
|
-
* @param factory - Factory function for service instantiation with container
|
|
1209
|
-
* @returns
|
|
1615
|
+
* @param factory - Factory function for service instantiation with container
|
|
1616
|
+
* @returns The service layer
|
|
1210
1617
|
*
|
|
1211
1618
|
* @example Simple service without dependencies
|
|
1212
1619
|
* ```typescript
|
|
@@ -1231,25 +1638,32 @@ interface Service<T extends AnyTag> extends Layer<ServiceDependencies<T>, T> {
|
|
|
1231
1638
|
* getUsers() { return this.db.query(); }
|
|
1232
1639
|
* }
|
|
1233
1640
|
*
|
|
1234
|
-
* const userService = service(UserService, async (
|
|
1235
|
-
* new UserService(await
|
|
1641
|
+
* const userService = service(UserService, async (ctx) =>
|
|
1642
|
+
* new UserService(await ctx.get(DatabaseService))
|
|
1236
1643
|
* );
|
|
1237
1644
|
* ```
|
|
1645
|
+
*/
|
|
1646
|
+
declare function service<T extends AnyTag>(serviceClass: T, spec: DependencySpec<T, ServiceDependencies<T>>): Layer<ServiceDependencies<T>, T>;
|
|
1647
|
+
//#endregion
|
|
1648
|
+
//#region src/value.d.ts
|
|
1649
|
+
/**
|
|
1650
|
+
* Creates a layer that provides a constant value for a given tag.
|
|
1238
1651
|
*
|
|
1239
|
-
* @
|
|
1652
|
+
* @param tag - The value tag to provide
|
|
1653
|
+
* @param constantValue - The constant value to provide
|
|
1654
|
+
* @returns A layer with no dependencies that provides the constant value
|
|
1655
|
+
*
|
|
1656
|
+
* @example
|
|
1240
1657
|
* ```typescript
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
* super();
|
|
1244
|
-
* }
|
|
1245
|
-
* }
|
|
1658
|
+
* const ApiKey = Tag.of('ApiKey')<string>();
|
|
1659
|
+
* const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
|
|
1246
1660
|
*
|
|
1247
|
-
* const
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
* );
|
|
1661
|
+
* const apiKey = value(ApiKey, 'my-secret-key');
|
|
1662
|
+
* const dbUrl = value(DatabaseUrl, 'postgresql://localhost:5432/myapp');
|
|
1663
|
+
*
|
|
1664
|
+
* const config = Layer.merge(apiKey, dbUrl);
|
|
1251
1665
|
* ```
|
|
1252
1666
|
*/
|
|
1253
|
-
declare function
|
|
1667
|
+
declare function value<T, Id extends string | symbol>(tag: ValueTag<T, Id>, constantValue: T): Layer<never, ValueTag<T, Id>>;
|
|
1254
1668
|
//#endregion
|
|
1255
|
-
export { type Inject, Layer, type
|
|
1669
|
+
export { type AnyLayer, type AnyTag, CircularDependencyError, type ClassTag, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, type DependencyLifecycle, type Factory, type Finalizer, type IContainer, type Inject, Layer, type PromiseOrValue, type ResolutionContext, type Scope, ScopedContainer, Tag, type TagType, UnknownDependencyError, type ValueTag, container, layer, scoped, service, value };
|