sandly 0.0.2
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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/index.d.ts +1255 -0
- package/dist/index.js +983 -0
- package/package.json +77 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
//#region src/tag.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Internal symbol used to identify tagged types within the dependency injection system.
|
|
4
|
+
* This symbol is used as a property key to attach metadata to both value tags and class tags.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
declare const TagId: "__tag_id__";
|
|
8
|
+
/**
|
|
9
|
+
* Type representing a value-based dependency tag.
|
|
10
|
+
*
|
|
11
|
+
* Value tags are used to represent non-class dependencies like configuration objects,
|
|
12
|
+
* strings, numbers, or any other values. They use phantom types to maintain type safety
|
|
13
|
+
* while being distinguishable at runtime through their unique identifiers.
|
|
14
|
+
*
|
|
15
|
+
* @template T - The type of the value this tag represents
|
|
16
|
+
* @template Id - The unique identifier for this tag (string or symbol)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // Creates a value tag for string configuration
|
|
21
|
+
* const ApiKeyTag: ValueTag<string, 'apiKey'> = Tag.of('apiKey')<string>();
|
|
22
|
+
*
|
|
23
|
+
* // Register in container
|
|
24
|
+
* container.register(ApiKeyTag, () => 'my-secret-key');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
type ValueTag<T, Id extends string | symbol> = Readonly<{
|
|
28
|
+
readonly [TagId]: Id;
|
|
29
|
+
/** @internal Phantom type to carry T */
|
|
30
|
+
readonly __type: T;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Type representing a class-based dependency tag.
|
|
34
|
+
*
|
|
35
|
+
* Tagged classes are created by Tag.Class() and serve as both the dependency identifier
|
|
36
|
+
* and the constructor for the service. They extend regular classes with tag metadata
|
|
37
|
+
* that the DI system uses for identification and type safety.
|
|
38
|
+
*
|
|
39
|
+
* @template T - The type of instances created by this tagged class
|
|
40
|
+
* @template Id - The unique identifier for this tag (string or symbol)
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // Creates a tagged class
|
|
45
|
+
* class UserService extends Tag.Class('UserService') {
|
|
46
|
+
* getUsers() { return []; }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* // Register in container
|
|
50
|
+
* container.register(UserService, () => new UserService());
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @internal - Users should use Tag.Class() instead of working with this type directly
|
|
54
|
+
*/
|
|
55
|
+
type TaggedClass<T, Id extends string | symbol> = {
|
|
56
|
+
new (...args: any[]): T & {
|
|
57
|
+
readonly [TagId]: Id;
|
|
58
|
+
};
|
|
59
|
+
readonly [TagId]: Id;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Type representing a class-based dependency tag.
|
|
63
|
+
*
|
|
64
|
+
* This type is a shortcut for TaggedClass<T, string | symbol>.
|
|
65
|
+
*
|
|
66
|
+
* @template T - The type of instances created by this tagged class
|
|
67
|
+
* @returns A tagged class with a string or symbol identifier
|
|
68
|
+
*
|
|
69
|
+
* @internal - Users should use Tag.Class() instead of working with this type directly
|
|
70
|
+
*/
|
|
71
|
+
type ClassTag<T> = TaggedClass<T, string | symbol>;
|
|
72
|
+
/**
|
|
73
|
+
* Union type representing any valid dependency tag in the system.
|
|
74
|
+
*
|
|
75
|
+
* A tag can be either a value tag (for non-class dependencies) or a tagged class
|
|
76
|
+
* (for service classes). This type is used throughout the DI system to constrain
|
|
77
|
+
* what can be used as a dependency identifier.
|
|
78
|
+
*
|
|
79
|
+
* @example Value tag
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
82
|
+
* // ConfigTag satisfies AnyTag
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example Class tag
|
|
86
|
+
* ```typescript
|
|
87
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
88
|
+
* // DatabaseService satisfies AnyTag
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
type AnyTag = ValueTag<any, string | symbol> | TaggedClass<any, string | symbol>;
|
|
92
|
+
/**
|
|
93
|
+
* Utility object containing factory functions for creating dependency tags.
|
|
94
|
+
*
|
|
95
|
+
* The Tag object provides the primary API for creating both value tags and class tags
|
|
96
|
+
* used throughout the dependency injection system. It's the main entry point for
|
|
97
|
+
* defining dependencies in a type-safe way.
|
|
98
|
+
*/
|
|
99
|
+
declare const Tag: {
|
|
100
|
+
/**
|
|
101
|
+
* Creates a value tag factory for dependencies that are not classes.
|
|
102
|
+
*
|
|
103
|
+
* This method returns a factory function that, when called with a type parameter,
|
|
104
|
+
* creates a value tag for that type. The tag has a string or symbol-based identifier
|
|
105
|
+
* that must be unique within your application.
|
|
106
|
+
*
|
|
107
|
+
* @template Id - The string or symbol identifier for this tag (must be unique)
|
|
108
|
+
* @param id - The unique string or symbol identifier for this tag
|
|
109
|
+
* @returns A factory function that creates value tags for the specified type
|
|
110
|
+
*
|
|
111
|
+
* @example Basic usage with strings
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
114
|
+
* const ConfigTag = Tag.of('config')<{ dbUrl: string; port: number }>();
|
|
115
|
+
*
|
|
116
|
+
* container
|
|
117
|
+
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
118
|
+
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost', port: 5432 }));
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @example Usage with symbols
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const DB_CONFIG_SYM = Symbol('database-config');
|
|
124
|
+
* const ConfigTag = Tag.of(DB_CONFIG_SYM)<DatabaseConfig>();
|
|
125
|
+
*
|
|
126
|
+
* container.register(ConfigTag, () => ({ host: 'localhost', port: 5432 }));
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example Primitive values
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const PortTag = Tag.of('port')<number>();
|
|
132
|
+
* const EnabledTag = Tag.of('enabled')<boolean>();
|
|
133
|
+
*
|
|
134
|
+
* container
|
|
135
|
+
* .register(PortTag, () => 3000)
|
|
136
|
+
* .register(EnabledTag, () => true);
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @example Complex objects
|
|
140
|
+
* ```typescript
|
|
141
|
+
* interface DatabaseConfig {
|
|
142
|
+
* host: string;
|
|
143
|
+
* port: number;
|
|
144
|
+
* database: string;
|
|
145
|
+
* }
|
|
146
|
+
*
|
|
147
|
+
* const DbConfigTag = Tag.of('database-config')<DatabaseConfig>();
|
|
148
|
+
* container.register(DbConfigTag, () => ({
|
|
149
|
+
* host: 'localhost',
|
|
150
|
+
* port: 5432,
|
|
151
|
+
* database: 'myapp'
|
|
152
|
+
* }));
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
of: <Id extends string | symbol>(id: Id) => <T>() => ValueTag<T, Id>;
|
|
156
|
+
/**
|
|
157
|
+
* Creates an anonymous value tag with a unique symbol identifier.
|
|
158
|
+
*
|
|
159
|
+
* This is useful when you want a tag that's guaranteed to be unique but don't
|
|
160
|
+
* need a human-readable identifier. Each call creates a new unique symbol,
|
|
161
|
+
* making it impossible to accidentally create duplicate tags.
|
|
162
|
+
*
|
|
163
|
+
* @template T - The type that this tag represents
|
|
164
|
+
* @returns A value tag with a unique symbol identifier
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* interface InternalConfig {
|
|
169
|
+
* secretKey: string;
|
|
170
|
+
* }
|
|
171
|
+
*
|
|
172
|
+
* const InternalConfigTag = Tag.for<InternalConfig>();
|
|
173
|
+
*
|
|
174
|
+
* // This tag is guaranteed to be unique - no chance of conflicts
|
|
175
|
+
* container.register(InternalConfigTag, () => ({
|
|
176
|
+
* secretKey: generateSecret()
|
|
177
|
+
* }));
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* @example Multiple anonymous tags
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const ConfigA = Tag.for<string>();
|
|
183
|
+
* const ConfigB = Tag.for<string>();
|
|
184
|
+
*
|
|
185
|
+
* // These are different tags even though they have the same type
|
|
186
|
+
* console.log(ConfigA === ConfigB); // false
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
for: <T>() => ValueTag<T, symbol>;
|
|
190
|
+
/**
|
|
191
|
+
* Creates a base class that can be extended to create service classes with dependency tags.
|
|
192
|
+
*
|
|
193
|
+
* This is the primary way to define service classes in the dependency injection system.
|
|
194
|
+
* Classes that extend the returned base class become both the dependency identifier
|
|
195
|
+
* and the implementation, providing type safety and clear semantics.
|
|
196
|
+
*
|
|
197
|
+
* @template Id - The unique identifier for this service class
|
|
198
|
+
* @param id - The unique identifier (string or symbol) for this service
|
|
199
|
+
* @returns A base class that can be extended to create tagged service classes
|
|
200
|
+
*
|
|
201
|
+
* @example Basic service class
|
|
202
|
+
* ```typescript
|
|
203
|
+
* class UserService extends Tag.Class('UserService') {
|
|
204
|
+
* getUsers() {
|
|
205
|
+
* return ['alice', 'bob'];
|
|
206
|
+
* }
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* container.register(UserService, () => new UserService());
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @example Service with dependencies
|
|
213
|
+
* ```typescript
|
|
214
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
215
|
+
* query(sql: string) { return []; }
|
|
216
|
+
* }
|
|
217
|
+
*
|
|
218
|
+
* class UserRepository extends Tag.Class('UserRepository') {
|
|
219
|
+
* constructor(private db: DatabaseService) {
|
|
220
|
+
* super();
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* findUser(id: string) {
|
|
224
|
+
* return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
|
225
|
+
* }
|
|
226
|
+
* }
|
|
227
|
+
*
|
|
228
|
+
* container
|
|
229
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
230
|
+
* .register(UserRepository, async (c) =>
|
|
231
|
+
* new UserRepository(await c.get(DatabaseService))
|
|
232
|
+
* );
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* @example With symbol identifiers
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const SERVICE_ID = Symbol('InternalService');
|
|
238
|
+
*
|
|
239
|
+
* class InternalService extends Tag.Class(SERVICE_ID) {
|
|
240
|
+
* doInternalWork() { return 'work'; }
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
Class: <Id extends string | symbol>(id: Id) => TaggedClass<{
|
|
245
|
+
/** @internal */
|
|
246
|
+
readonly __type: unknown;
|
|
247
|
+
readonly __tag_id__: Id;
|
|
248
|
+
}, Id>;
|
|
249
|
+
/**
|
|
250
|
+
* Extracts the string representation of a tag's identifier.
|
|
251
|
+
*
|
|
252
|
+
* This utility function returns a human-readable string for any tag's identifier,
|
|
253
|
+
* whether it's a string-based or symbol-based tag. Primarily used internally
|
|
254
|
+
* for error messages and debugging.
|
|
255
|
+
*
|
|
256
|
+
* @param tag - Any valid dependency tag (value tag or class tag)
|
|
257
|
+
* @returns String representation of the tag's identifier
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* const StringTag = Tag.of('myString')<string>();
|
|
262
|
+
* const SymbolTag = Tag.for<number>();
|
|
263
|
+
* class ServiceClass extends Tag.Class('MyService') {}
|
|
264
|
+
*
|
|
265
|
+
* console.log(Tag.id(StringTag)); // "myString"
|
|
266
|
+
* console.log(Tag.id(SymbolTag)); // "Symbol()"
|
|
267
|
+
* console.log(Tag.id(ServiceClass)); // "MyService"
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @internal - Primarily for internal use in error messages and debugging
|
|
271
|
+
*/
|
|
272
|
+
id: (tag: AnyTag) => string;
|
|
273
|
+
};
|
|
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
|
+
//#endregion
|
|
314
|
+
//#region src/types.d.ts
|
|
315
|
+
type PromiseOrValue<T> = T | Promise<T>;
|
|
316
|
+
/**
|
|
317
|
+
* Unique symbol used to store the original ValueTag in Inject<T> types.
|
|
318
|
+
* This prevents property name collisions while allowing type-level extraction.
|
|
319
|
+
*/
|
|
320
|
+
declare const InjectSource: unique symbol;
|
|
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
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Type representing a factory function used to create dependency instances.
|
|
333
|
+
*
|
|
334
|
+
* Factory functions are the core mechanism for dependency creation in the DI system.
|
|
335
|
+
* They receive a dependency container and can use it to resolve other dependencies
|
|
336
|
+
* that the service being created needs.
|
|
337
|
+
*
|
|
338
|
+
* The factory can be either synchronous (returning T directly) or asynchronous
|
|
339
|
+
* (returning Promise<T>). The container handles both cases transparently.
|
|
340
|
+
*
|
|
341
|
+
* @template T - The type of the service instance being created
|
|
342
|
+
* @template TReg - Union type of all dependencies available in the container
|
|
343
|
+
*
|
|
344
|
+
* @example Synchronous factory
|
|
345
|
+
* ```typescript
|
|
346
|
+
* const factory: Factory<DatabaseService, never> = (container) => {
|
|
347
|
+
* return new DatabaseService('sqlite://memory');
|
|
348
|
+
* };
|
|
349
|
+
* ```
|
|
350
|
+
*
|
|
351
|
+
* @example Asynchronous factory with dependencies
|
|
352
|
+
* ```typescript
|
|
353
|
+
* const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (container) => {
|
|
354
|
+
* const [config, db] = await Promise.all([
|
|
355
|
+
* container.get(ConfigTag),
|
|
356
|
+
* container.get(DatabaseService)
|
|
357
|
+
* ]);
|
|
358
|
+
* return new UserService(config, db);
|
|
359
|
+
* };
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
type Factory<T, TReg extends AnyTag, TScope extends Scope> = (container: IContainer<TReg, TScope>) => PromiseOrValue<T>;
|
|
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;
|
|
398
|
+
/**
|
|
399
|
+
* Type representing a finalizer function used to clean up dependency instances.
|
|
400
|
+
*
|
|
401
|
+
* Finalizers are optional cleanup functions that are called when the container
|
|
402
|
+
* is destroyed via `container.destroy()`. They receive the created instance
|
|
403
|
+
* and should perform any necessary cleanup (closing connections, releasing resources, etc.).
|
|
404
|
+
*
|
|
405
|
+
* Like factories, finalizers can be either synchronous or asynchronous.
|
|
406
|
+
* All finalizers are called concurrently during container destruction.
|
|
407
|
+
*
|
|
408
|
+
* @template T - The type of the service instance being finalized
|
|
409
|
+
*
|
|
410
|
+
* @example Synchronous finalizer
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const finalizer: Finalizer<FileHandle> = (fileHandle) => {
|
|
413
|
+
* fileHandle.close();
|
|
414
|
+
* };
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* @example Asynchronous finalizer
|
|
418
|
+
* ```typescript
|
|
419
|
+
* const finalizer: Finalizer<DatabaseConnection> = async (connection) => {
|
|
420
|
+
* await connection.disconnect();
|
|
421
|
+
* };
|
|
422
|
+
* ```
|
|
423
|
+
*
|
|
424
|
+
* @example Resilient finalizer
|
|
425
|
+
* ```typescript
|
|
426
|
+
* const finalizer: Finalizer<HttpServer> = async (server) => {
|
|
427
|
+
* try {
|
|
428
|
+
* await server.close();
|
|
429
|
+
* } catch (error) {
|
|
430
|
+
* if (!error.message.includes('already closed')) {
|
|
431
|
+
* throw error; // Re-throw unexpected errors
|
|
432
|
+
* }
|
|
433
|
+
* // Ignore "already closed" errors
|
|
434
|
+
* }
|
|
435
|
+
* };
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
|
|
439
|
+
type Scope = string | symbol;
|
|
440
|
+
declare const DefaultScope: unique symbol;
|
|
441
|
+
type DefaultScope = typeof DefaultScope;
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/container.d.ts
|
|
444
|
+
type DependencyLifecycle<T extends AnyTag, TReg extends AnyTag, TScope extends Scope> = {
|
|
445
|
+
factory: Factory<ServiceOf<T>, TReg, TScope>;
|
|
446
|
+
finalizer: Finalizer<ServiceOf<T>>;
|
|
447
|
+
};
|
|
448
|
+
interface IContainer<TReg extends AnyTag, TScope extends Scope = DefaultScope> {
|
|
449
|
+
register<T extends AnyTag>(tag: T, factoryOrLifecycle: Factory<ServiceOf<T>, TReg, TScope> | DependencyLifecycle<T, TReg, TScope>, scope?: TScope): IContainer<TReg | T, TScope>;
|
|
450
|
+
has(tag: AnyTag): boolean;
|
|
451
|
+
get<T extends TReg>(tag: T): Promise<ServiceOf<T>>;
|
|
452
|
+
destroy(): Promise<void>;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* A type-safe dependency injection container that manages service instantiation,
|
|
456
|
+
* caching, and lifecycle management with support for async dependencies and
|
|
457
|
+
* circular dependency detection.
|
|
458
|
+
*
|
|
459
|
+
* The container maintains complete type safety by tracking registered dependencies
|
|
460
|
+
* at the type level, ensuring that only registered dependencies can be retrieved
|
|
461
|
+
* and preventing runtime errors.
|
|
462
|
+
*
|
|
463
|
+
* @template TReg - Union type of all registered dependency tags in this container
|
|
464
|
+
*
|
|
465
|
+
* @example Basic usage with class tags
|
|
466
|
+
* ```typescript
|
|
467
|
+
* import { container, Tag } from 'sandl';
|
|
468
|
+
*
|
|
469
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
470
|
+
* query() { return 'data'; }
|
|
471
|
+
* }
|
|
472
|
+
*
|
|
473
|
+
* class UserService extends Tag.Class('UserService') {
|
|
474
|
+
* constructor(private db: DatabaseService) {}
|
|
475
|
+
* getUser() { return this.db.query(); }
|
|
476
|
+
* }
|
|
477
|
+
*
|
|
478
|
+
* const c = container()
|
|
479
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
480
|
+
* .register(UserService, async (container) =>
|
|
481
|
+
* new UserService(await container.get(DatabaseService))
|
|
482
|
+
* );
|
|
483
|
+
*
|
|
484
|
+
* const userService = await c.get(UserService);
|
|
485
|
+
* ```
|
|
486
|
+
*
|
|
487
|
+
* @example Usage with value tags
|
|
488
|
+
* ```typescript
|
|
489
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
490
|
+
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
491
|
+
*
|
|
492
|
+
* const c = container()
|
|
493
|
+
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
494
|
+
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
495
|
+
*
|
|
496
|
+
* const apiKey = await c.get(ApiKeyTag);
|
|
497
|
+
* const config = await c.get(ConfigTag);
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @example With finalizers for cleanup
|
|
501
|
+
* ```typescript
|
|
502
|
+
* class DatabaseConnection extends Tag.Class('DatabaseConnection') {
|
|
503
|
+
* async connect() { return; }
|
|
504
|
+
* async disconnect() { return; }
|
|
505
|
+
* }
|
|
506
|
+
*
|
|
507
|
+
* const c = container().register(
|
|
508
|
+
* DatabaseConnection,
|
|
509
|
+
* async () => {
|
|
510
|
+
* const conn = new DatabaseConnection();
|
|
511
|
+
* await conn.connect();
|
|
512
|
+
* return conn;
|
|
513
|
+
* },
|
|
514
|
+
* async (conn) => conn.disconnect() // Finalizer for cleanup
|
|
515
|
+
* );
|
|
516
|
+
*
|
|
517
|
+
* // Later...
|
|
518
|
+
* await c.destroy(); // Calls all finalizers
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
declare class Container<TReg extends AnyTag> implements IContainer<TReg> {
|
|
522
|
+
/**
|
|
523
|
+
* Cache of instantiated dependencies as promises.
|
|
524
|
+
* Ensures singleton behavior and supports concurrent access.
|
|
525
|
+
* @internal
|
|
526
|
+
*/
|
|
527
|
+
private readonly cache;
|
|
528
|
+
/**
|
|
529
|
+
* Factory functions for creating dependency instances.
|
|
530
|
+
* @internal
|
|
531
|
+
*/
|
|
532
|
+
private readonly factories;
|
|
533
|
+
/**
|
|
534
|
+
* Finalizer functions for cleaning up dependencies when the container is destroyed.
|
|
535
|
+
* @internal
|
|
536
|
+
*/
|
|
537
|
+
private readonly finalizers;
|
|
538
|
+
/**
|
|
539
|
+
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
540
|
+
*
|
|
541
|
+
* The factory function receives the current container instance and must return the
|
|
542
|
+
* service instance (or a Promise of it). The container tracks the registration at
|
|
543
|
+
* the type level, ensuring type safety for subsequent `.get()` calls.
|
|
544
|
+
*
|
|
545
|
+
* @template T - The dependency tag being registered
|
|
546
|
+
* @param tag - The dependency tag (class or value tag)
|
|
547
|
+
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
548
|
+
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
549
|
+
* @returns A new container instance with the dependency registered
|
|
550
|
+
* @throws {DependencyContainerError} If the dependency is already registered
|
|
551
|
+
*
|
|
552
|
+
* @example Registering a simple service
|
|
553
|
+
* ```typescript
|
|
554
|
+
* class LoggerService extends Tag.Class('LoggerService') {
|
|
555
|
+
* log(message: string) { console.log(message); }
|
|
556
|
+
* }
|
|
557
|
+
*
|
|
558
|
+
* const c = container().register(
|
|
559
|
+
* LoggerService,
|
|
560
|
+
* () => new LoggerService()
|
|
561
|
+
* );
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* @example Registering with dependencies
|
|
565
|
+
* ```typescript
|
|
566
|
+
* class UserService extends Tag.Class('UserService') {
|
|
567
|
+
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
568
|
+
* }
|
|
569
|
+
*
|
|
570
|
+
* const c = container()
|
|
571
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
572
|
+
* .register(LoggerService, () => new LoggerService())
|
|
573
|
+
* .register(UserService, async (container) =>
|
|
574
|
+
* new UserService(
|
|
575
|
+
* await container.get(DatabaseService),
|
|
576
|
+
* await container.get(LoggerService)
|
|
577
|
+
* )
|
|
578
|
+
* );
|
|
579
|
+
* ```
|
|
580
|
+
*
|
|
581
|
+
* @example Using value tags
|
|
582
|
+
* ```typescript
|
|
583
|
+
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
584
|
+
*
|
|
585
|
+
* const c = container().register(
|
|
586
|
+
* ConfigTag,
|
|
587
|
+
* () => ({ apiUrl: 'https://api.example.com' })
|
|
588
|
+
* );
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @example With finalizer for cleanup
|
|
592
|
+
* ```typescript
|
|
593
|
+
* class DatabaseConnection extends Tag.Class('DatabaseConnection') {
|
|
594
|
+
* async connect() { return; }
|
|
595
|
+
* async close() { return; }
|
|
596
|
+
* }
|
|
597
|
+
*
|
|
598
|
+
* const c = container().register(
|
|
599
|
+
* DatabaseConnection,
|
|
600
|
+
* async () => {
|
|
601
|
+
* const conn = new DatabaseConnection();
|
|
602
|
+
* await conn.connect();
|
|
603
|
+
* return conn;
|
|
604
|
+
* },
|
|
605
|
+
* (conn) => conn.close() // Called during container.destroy()
|
|
606
|
+
* );
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
register<T extends AnyTag>(tag: T, factoryOrLifecycle: Factory<ServiceOf<T>, TReg, DefaultScope> | DependencyLifecycle<T, TReg, DefaultScope>): IContainer<TReg | T>;
|
|
610
|
+
/**
|
|
611
|
+
* Checks if a dependency has been instantiated (cached) in the container.
|
|
612
|
+
*
|
|
613
|
+
* Note: This returns `true` only after the dependency has been created via `.get()`.
|
|
614
|
+
* A registered but not-yet-instantiated dependency will return `false`.
|
|
615
|
+
*
|
|
616
|
+
* @param tag - The dependency tag to check
|
|
617
|
+
* @returns `true` if the dependency has been instantiated and cached, `false` otherwise
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```typescript
|
|
621
|
+
* 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
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
has(tag: AnyTag): boolean;
|
|
630
|
+
/**
|
|
631
|
+
* Retrieves a dependency instance from the container, creating it if necessary.
|
|
632
|
+
*
|
|
633
|
+
* This method ensures singleton behavior - each dependency is created only once
|
|
634
|
+
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
635
|
+
* requests for the same dependency correctly.
|
|
636
|
+
*
|
|
637
|
+
* The method performs circular dependency detection using AsyncLocalStorage to track
|
|
638
|
+
* the resolution chain across async boundaries.
|
|
639
|
+
*
|
|
640
|
+
* @template T - The dependency tag type (must be registered in this container)
|
|
641
|
+
* @param tag - The dependency tag to retrieve
|
|
642
|
+
* @returns Promise resolving to the service instance
|
|
643
|
+
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
644
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
645
|
+
* @throws {DependencyCreationError} If the factory function throws an error
|
|
646
|
+
*
|
|
647
|
+
* @example Basic usage
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const c = container()
|
|
650
|
+
* .register(DatabaseService, () => new DatabaseService());
|
|
651
|
+
*
|
|
652
|
+
* const db = await c.get(DatabaseService);
|
|
653
|
+
* db.query('SELECT * FROM users');
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* @example Concurrent access (singleton behavior)
|
|
657
|
+
* ```typescript
|
|
658
|
+
* // All three calls will receive the same instance
|
|
659
|
+
* const [db1, db2, db3] = await Promise.all([
|
|
660
|
+
* c.get(DatabaseService),
|
|
661
|
+
* c.get(DatabaseService),
|
|
662
|
+
* c.get(DatabaseService)
|
|
663
|
+
* ]);
|
|
664
|
+
*
|
|
665
|
+
* console.log(db1 === db2 === db3); // true
|
|
666
|
+
* ```
|
|
667
|
+
*
|
|
668
|
+
* @example Dependency injection in factories
|
|
669
|
+
* ```typescript
|
|
670
|
+
* const c = container()
|
|
671
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
672
|
+
* .register(UserService, async (container) => {
|
|
673
|
+
* const db = await container.get(DatabaseService);
|
|
674
|
+
* return new UserService(db);
|
|
675
|
+
* });
|
|
676
|
+
*
|
|
677
|
+
* const userService = await c.get(UserService);
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
get<T extends TReg>(tag: T): Promise<ServiceOf<T>>;
|
|
681
|
+
/**
|
|
682
|
+
* Destroys all instantiated dependencies by calling their finalizers, then clears the instance cache.
|
|
683
|
+
*
|
|
684
|
+
* **Important: This method preserves the container structure (factories and finalizers) for reuse.**
|
|
685
|
+
* The container can be used again after destruction to create fresh instances following the same
|
|
686
|
+
* dependency patterns.
|
|
687
|
+
*
|
|
688
|
+
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
689
|
+
* for maximum cleanup performance.
|
|
690
|
+
* If any finalizers fail, all errors are collected and a DependencyContainerFinalizationError
|
|
691
|
+
* is thrown containing details of all failures.
|
|
692
|
+
*
|
|
693
|
+
* **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
|
|
694
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
695
|
+
* dependencies are cleaned up.
|
|
696
|
+
*
|
|
697
|
+
* @returns Promise that resolves when all cleanup is complete
|
|
698
|
+
* @throws {DependencyContainerFinalizationError} If any finalizers fail during cleanup
|
|
699
|
+
*
|
|
700
|
+
* @example Basic cleanup and reuse
|
|
701
|
+
* ```typescript
|
|
702
|
+
* const c = container()
|
|
703
|
+
* .register(DatabaseConnection,
|
|
704
|
+
* async () => {
|
|
705
|
+
* const conn = new DatabaseConnection();
|
|
706
|
+
* await conn.connect();
|
|
707
|
+
* return conn;
|
|
708
|
+
* },
|
|
709
|
+
* (conn) => conn.disconnect() // Finalizer
|
|
710
|
+
* );
|
|
711
|
+
*
|
|
712
|
+
* // First use cycle
|
|
713
|
+
* const db1 = await c.get(DatabaseConnection);
|
|
714
|
+
* await c.destroy(); // Calls conn.disconnect(), clears cache
|
|
715
|
+
*
|
|
716
|
+
* // Container can be reused - creates fresh instances
|
|
717
|
+
* const db2 = await c.get(DatabaseConnection); // New connection
|
|
718
|
+
* expect(db2).not.toBe(db1); // Different instances
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @example Multiple destroy/reuse cycles
|
|
722
|
+
* ```typescript
|
|
723
|
+
* const c = container().register(UserService, () => new UserService());
|
|
724
|
+
*
|
|
725
|
+
* for (let i = 0; i < 5; i++) {
|
|
726
|
+
* const user = await c.get(UserService);
|
|
727
|
+
* // ... use service ...
|
|
728
|
+
* await c.destroy(); // Clean up, ready for next cycle
|
|
729
|
+
* }
|
|
730
|
+
* ```
|
|
731
|
+
*
|
|
732
|
+
* @example Handling cleanup errors
|
|
733
|
+
* ```typescript
|
|
734
|
+
* try {
|
|
735
|
+
* await container.destroy();
|
|
736
|
+
* } catch (error) {
|
|
737
|
+
* if (error instanceof DependencyContainerFinalizationError) {
|
|
738
|
+
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
739
|
+
* }
|
|
740
|
+
* }
|
|
741
|
+
* // Container is still reusable even after finalizer errors
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
destroy(): Promise<void>;
|
|
745
|
+
}
|
|
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
|
+
/**
|
|
832
|
+
* Creates a new empty dependency injection container.
|
|
833
|
+
*
|
|
834
|
+
* This is a convenience factory function that creates a new DependencyContainer instance.
|
|
835
|
+
* The returned container starts with no registered dependencies and the type parameter
|
|
836
|
+
* defaults to `never`, indicating no dependencies are available for retrieval yet.
|
|
837
|
+
*
|
|
838
|
+
* @returns A new empty DependencyContainer instance
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* ```typescript
|
|
842
|
+
* import { container, Tag } from 'sandl';
|
|
843
|
+
*
|
|
844
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
845
|
+
* class UserService extends Tag.Class('UserService') {}
|
|
846
|
+
*
|
|
847
|
+
* const c = container()
|
|
848
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
849
|
+
* .register(UserService, async (container) =>
|
|
850
|
+
* new UserService(await container.get(DatabaseService))
|
|
851
|
+
* );
|
|
852
|
+
*
|
|
853
|
+
* const userService = await c.get(UserService);
|
|
854
|
+
* ```
|
|
855
|
+
*/
|
|
856
|
+
declare function container(): Container<never>;
|
|
857
|
+
declare function scopedContainer<TScope extends Scope>(scope: TScope): ScopedContainer<never, TScope>;
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/layer.d.ts
|
|
860
|
+
/**
|
|
861
|
+
* A dependency layer represents a reusable, composable unit of dependency registrations.
|
|
862
|
+
* Layers allow you to organize your dependency injection setup into logical groups
|
|
863
|
+
* that can be combined and reused across different contexts.
|
|
864
|
+
*
|
|
865
|
+
* @template TRequires - The union of tags this layer requires to be satisfied by other layers
|
|
866
|
+
* @template TProvides - The union of tags this layer provides/registers
|
|
867
|
+
*
|
|
868
|
+
* @example Basic layer usage
|
|
869
|
+
* ```typescript
|
|
870
|
+
* import { layer, Tag, container } from 'sandl';
|
|
871
|
+
*
|
|
872
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
873
|
+
* query() { return 'data'; }
|
|
874
|
+
* }
|
|
875
|
+
*
|
|
876
|
+
* // Create a layer that provides DatabaseService
|
|
877
|
+
* const databaseLayer = layer<never, typeof DatabaseService>((container) =>
|
|
878
|
+
* container.register(DatabaseService, () => new DatabaseService())
|
|
879
|
+
* );
|
|
880
|
+
*
|
|
881
|
+
* // Apply the layer to a container
|
|
882
|
+
* const c = container();
|
|
883
|
+
* const finalContainer = databaseLayer().register(c);
|
|
884
|
+
*
|
|
885
|
+
* const db = await finalContainer.get(DatabaseService);
|
|
886
|
+
* ```
|
|
887
|
+
*
|
|
888
|
+
* @example Layer composition
|
|
889
|
+
* ```typescript
|
|
890
|
+
* // Layer that requires DatabaseService and provides UserService
|
|
891
|
+
* const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
|
|
892
|
+
* container.register(UserService, async (c) =>
|
|
893
|
+
* new UserService(await c.get(DatabaseService))
|
|
894
|
+
* )
|
|
895
|
+
* );
|
|
896
|
+
*
|
|
897
|
+
* // Compose layers: database layer provides what user layer needs
|
|
898
|
+
* const appLayer = databaseLayer().to(userLayer());
|
|
899
|
+
* ```
|
|
900
|
+
*/
|
|
901
|
+
interface Layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never> {
|
|
902
|
+
/**
|
|
903
|
+
* Applies this layer's registrations to the given container.
|
|
904
|
+
*
|
|
905
|
+
* @param container - The container to register dependencies into
|
|
906
|
+
* @returns A new container with this layer's dependencies registered
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* ```typescript
|
|
910
|
+
* const container = container();
|
|
911
|
+
* const updatedContainer = myLayer.register(container);
|
|
912
|
+
* ```
|
|
913
|
+
*/
|
|
914
|
+
register: <TScope extends Scope>(container: IContainer<TRequires, TScope>) => IContainer<TRequires | TProvides, TScope>;
|
|
915
|
+
/**
|
|
916
|
+
* Composes this layer with a target layer, creating a pipeline where this layer's
|
|
917
|
+
* provisions satisfy the target layer's requirements. This creates a dependency
|
|
918
|
+
* flow from source → target.
|
|
919
|
+
*
|
|
920
|
+
* Type-safe: The target layer's requirements must be satisfiable by this layer's
|
|
921
|
+
* provisions and any remaining external requirements.
|
|
922
|
+
*
|
|
923
|
+
* @template TTargetRequires - What the target layer requires
|
|
924
|
+
* @template TTargetProvides - What the target layer provides
|
|
925
|
+
* @param target - The layer to compose with
|
|
926
|
+
* @returns A new composed layer
|
|
927
|
+
*
|
|
928
|
+
* @example Simple composition
|
|
929
|
+
* ```typescript
|
|
930
|
+
* const configLayer = layer<never, typeof ConfigTag>(...);
|
|
931
|
+
* const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
|
|
932
|
+
*
|
|
933
|
+
* // Config provides what database needs
|
|
934
|
+
* const infraLayer = configLayer().to(dbLayer());
|
|
935
|
+
* ```
|
|
936
|
+
*
|
|
937
|
+
* @example Multi-level composition
|
|
938
|
+
* ```typescript
|
|
939
|
+
* const appLayer = configLayer()
|
|
940
|
+
* .to(databaseLayer())
|
|
941
|
+
* .to(serviceLayer())
|
|
942
|
+
* .to(apiLayer());
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
945
|
+
to: <TTargetRequires extends AnyTag, TTargetProvides extends AnyTag>(target: Layer<TTargetRequires, TTargetProvides>) => Layer<TRequires | Exclude<TTargetRequires, TProvides>, TProvides | TTargetProvides>;
|
|
946
|
+
/**
|
|
947
|
+
* Merges this layer with another layer, combining their requirements and provisions.
|
|
948
|
+
* This is useful for combining independent layers that don't have a dependency
|
|
949
|
+
* relationship.
|
|
950
|
+
*
|
|
951
|
+
* @template TOtherRequires - What the other layer requires
|
|
952
|
+
* @template TOtherProvides - What the other layer provides
|
|
953
|
+
* @param other - The layer to merge with
|
|
954
|
+
* @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
|
|
955
|
+
*
|
|
956
|
+
* @example Merging independent layers
|
|
957
|
+
* ```typescript
|
|
958
|
+
* const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
|
|
959
|
+
* const loggingLayer = layer<never, typeof LoggerService>(...);
|
|
960
|
+
*
|
|
961
|
+
* // Combine infrastructure layers
|
|
962
|
+
* const infraLayer = persistenceLayer().and(loggingLayer());
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* @example Building complex layer combinations
|
|
966
|
+
* ```typescript
|
|
967
|
+
* const appInfraLayer = persistenceLayer()
|
|
968
|
+
* .and(messagingLayer())
|
|
969
|
+
* .and(observabilityLayer());
|
|
970
|
+
* ```
|
|
971
|
+
*/
|
|
972
|
+
and: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
|
|
973
|
+
}
|
|
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
|
+
/**
|
|
983
|
+
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
984
|
+
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
985
|
+
*
|
|
986
|
+
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
987
|
+
* @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
|
+
*
|
|
990
|
+
* @param register - Function that performs the dependency registrations. Receives a container and optional params.
|
|
991
|
+
* @returns A layer factory function. If TParams is undefined, returns a parameterless function. Otherwise returns a function that takes TParams.
|
|
992
|
+
*
|
|
993
|
+
* @example Simple layer without parameters
|
|
994
|
+
* ```typescript
|
|
995
|
+
* import { layer, Tag } from '@/di/layer.js';
|
|
996
|
+
*
|
|
997
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
998
|
+
* constructor(private url: string = 'sqlite://memory') {}
|
|
999
|
+
* query() { return 'data'; }
|
|
1000
|
+
* }
|
|
1001
|
+
*
|
|
1002
|
+
* // Layer that provides DatabaseService, requires nothing
|
|
1003
|
+
* const databaseLayer = layer<never, typeof DatabaseService>((container) =>
|
|
1004
|
+
* container.register(DatabaseService, () => new DatabaseService())
|
|
1005
|
+
* );
|
|
1006
|
+
*
|
|
1007
|
+
* // Usage
|
|
1008
|
+
* const dbLayerInstance = databaseLayer(); // No parameters needed
|
|
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 });
|
|
1039
|
+
* ```
|
|
1040
|
+
*
|
|
1041
|
+
* @example Complex application layer structure
|
|
1042
|
+
* ```typescript
|
|
1043
|
+
* // Configuration layer
|
|
1044
|
+
* const configLayer = layer<never, typeof ConfigTag>((container) =>
|
|
1045
|
+
* container.register(ConfigTag, () => loadConfig())
|
|
1046
|
+
* );
|
|
1047
|
+
*
|
|
1048
|
+
* // Infrastructure layer (requires config)
|
|
1049
|
+
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
1050
|
+
* (container) =>
|
|
1051
|
+
* container
|
|
1052
|
+
* .register(DatabaseService, async (c) => new DatabaseService(await c.get(ConfigTag)))
|
|
1053
|
+
* .register(CacheService, async (c) => new CacheService(await c.get(ConfigTag)))
|
|
1054
|
+
* );
|
|
1055
|
+
*
|
|
1056
|
+
* // Service layer (requires infrastructure)
|
|
1057
|
+
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
1058
|
+
* (container) =>
|
|
1059
|
+
* container.register(UserService, async (c) =>
|
|
1060
|
+
* new UserService(await c.get(DatabaseService), await c.get(CacheService))
|
|
1061
|
+
* )
|
|
1062
|
+
* );
|
|
1063
|
+
*
|
|
1064
|
+
* // Compose the complete application
|
|
1065
|
+
* const appLayer = configLayer().to(infraLayer()).to(serviceLayer());
|
|
1066
|
+
* ```
|
|
1067
|
+
*/
|
|
1068
|
+
declare function layer<TRequires extends AnyTag = never, TProvides extends AnyTag = never, TParams = undefined>(register: <TScope extends Scope>(container: IContainer<TRequires, TScope>, params: TParams) => IContainer<TRequires | TProvides, TScope>): LayerFactory<TRequires, TProvides, TParams>;
|
|
1069
|
+
/**
|
|
1070
|
+
* Helper type that extracts the union of all requirements from an array of layers.
|
|
1071
|
+
* Used by Layer.merge() to compute the correct requirement type for the merged layer.
|
|
1072
|
+
*
|
|
1073
|
+
* @internal
|
|
1074
|
+
*/
|
|
1075
|
+
type UnionOfRequires<T extends readonly Layer<AnyTag, AnyTag>[]> = { [K in keyof T]: T[K] extends Layer<infer R, AnyTag> ? R : never }[number];
|
|
1076
|
+
/**
|
|
1077
|
+
* Helper type that extracts the union of all provisions from an array of layers.
|
|
1078
|
+
* Used by Layer.merge() to compute the correct provision type for the merged layer.
|
|
1079
|
+
*
|
|
1080
|
+
* @internal
|
|
1081
|
+
*/
|
|
1082
|
+
type UnionOfProvides<T extends readonly Layer<AnyTag, AnyTag>[]> = { [K in keyof T]: T[K] extends Layer<AnyTag, infer P> ? P : never }[number];
|
|
1083
|
+
/**
|
|
1084
|
+
* Utility object containing helper functions for working with layers.
|
|
1085
|
+
*/
|
|
1086
|
+
declare const Layer: {
|
|
1087
|
+
/**
|
|
1088
|
+
* Creates an empty layer that provides no dependencies and requires no dependencies.
|
|
1089
|
+
* This is useful as a base layer or for testing.
|
|
1090
|
+
*
|
|
1091
|
+
* @returns An empty layer that can be used as a starting point for layer composition
|
|
1092
|
+
*
|
|
1093
|
+
* @example
|
|
1094
|
+
* ```typescript
|
|
1095
|
+
* import { Layer } from 'sandl';
|
|
1096
|
+
*
|
|
1097
|
+
* const baseLayer = Layer.empty();
|
|
1098
|
+
* const appLayer = baseLayer
|
|
1099
|
+
* .and(configLayer())
|
|
1100
|
+
* .and(serviceLayer());
|
|
1101
|
+
* ```
|
|
1102
|
+
*/
|
|
1103
|
+
empty(): Layer;
|
|
1104
|
+
/**
|
|
1105
|
+
* Merges multiple layers at once in a type-safe way.
|
|
1106
|
+
* This is equivalent to chaining `.and()` calls but more convenient for multiple layers.
|
|
1107
|
+
*
|
|
1108
|
+
* All layers are merged in order, combining their requirements and provisions.
|
|
1109
|
+
* The resulting layer requires the union of all input layer requirements and
|
|
1110
|
+
* provides the union of all input layer provisions.
|
|
1111
|
+
*
|
|
1112
|
+
* @template T - The tuple type of layers to merge
|
|
1113
|
+
* @param layers - At least 2 layers to merge together
|
|
1114
|
+
* @returns A new layer that combines all input layers
|
|
1115
|
+
*
|
|
1116
|
+
* @example Basic usage
|
|
1117
|
+
* ```typescript
|
|
1118
|
+
* import { Layer } from 'sandl';
|
|
1119
|
+
*
|
|
1120
|
+
* const infraLayer = Layer.merge(
|
|
1121
|
+
* databaseLayer(),
|
|
1122
|
+
* cacheLayer(),
|
|
1123
|
+
* loggingLayer()
|
|
1124
|
+
* );
|
|
1125
|
+
* ```
|
|
1126
|
+
*
|
|
1127
|
+
* @example Equivalent to chaining .and()
|
|
1128
|
+
* ```typescript
|
|
1129
|
+
* // These are equivalent:
|
|
1130
|
+
* const layer1 = Layer.merge(layerA(), layerB(), layerC());
|
|
1131
|
+
* const layer2 = layerA().and(layerB()).and(layerC());
|
|
1132
|
+
* ```
|
|
1133
|
+
*
|
|
1134
|
+
* @example Building infrastructure layers
|
|
1135
|
+
* ```typescript
|
|
1136
|
+
* const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
|
|
1137
|
+
* const messagingLayer = layer<never, typeof MessageQueue>(...);
|
|
1138
|
+
* const observabilityLayer = layer<never, typeof Logger | typeof Metrics>(...);
|
|
1139
|
+
*
|
|
1140
|
+
* // Merge all infrastructure concerns into one layer
|
|
1141
|
+
* const infraLayer = Layer.merge(
|
|
1142
|
+
* persistenceLayer(),
|
|
1143
|
+
* messagingLayer(),
|
|
1144
|
+
* observabilityLayer()
|
|
1145
|
+
* );
|
|
1146
|
+
*
|
|
1147
|
+
* // Now infraLayer provides: DatabaseService | CacheService | MessageQueue | Logger | Metrics
|
|
1148
|
+
* ```
|
|
1149
|
+
*/
|
|
1150
|
+
merge<T extends readonly [Layer<AnyTag, AnyTag>, Layer<AnyTag, AnyTag>, ...Layer<AnyTag, AnyTag>[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
|
|
1151
|
+
};
|
|
1152
|
+
//#endregion
|
|
1153
|
+
//#region src/service.d.ts
|
|
1154
|
+
/**
|
|
1155
|
+
* Extracts constructor parameter types from a TaggedClass.
|
|
1156
|
+
* Only parameters that extend AnyTag are considered as dependencies.
|
|
1157
|
+
*/
|
|
1158
|
+
type ConstructorParams<T extends ClassTag<unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
|
|
1159
|
+
/**
|
|
1160
|
+
* Helper to convert a tagged instance type back to its constructor type.
|
|
1161
|
+
* This uses the fact that tagged classes have a specific structure with TagId property.
|
|
1162
|
+
*/
|
|
1163
|
+
type InstanceToConstructorType<T> = T extends {
|
|
1164
|
+
readonly [TagId]: infer Id;
|
|
1165
|
+
} ? Id extends string | symbol ? TaggedClass<T, Id> : never : never;
|
|
1166
|
+
/**
|
|
1167
|
+
* Extracts constructor-typed dependencies from constructor parameters.
|
|
1168
|
+
* Converts instance types to their corresponding constructor types.
|
|
1169
|
+
* Handles both ClassTag dependencies (automatic) and ValueTag dependencies (via Inject helper).
|
|
1170
|
+
*/
|
|
1171
|
+
type FilterTags<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
|
|
1172
|
+
readonly [TagId]: string | symbol;
|
|
1173
|
+
} ? 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
|
+
/**
|
|
1179
|
+
* Extracts only the dependency tags from a constructor's parameters for ClassTag services,
|
|
1180
|
+
* or returns never for ValueTag services (which have no constructor dependencies).
|
|
1181
|
+
* This is used to determine what dependencies a service requires.
|
|
1182
|
+
*/
|
|
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
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
|
|
1197
|
+
*
|
|
1198
|
+
* For ClassTag services:
|
|
1199
|
+
* - Dependencies are automatically inferred from constructor parameters
|
|
1200
|
+
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
1201
|
+
*
|
|
1202
|
+
* For ValueTag services:
|
|
1203
|
+
* - No constructor dependencies are needed since they don't have constructors
|
|
1204
|
+
*
|
|
1205
|
+
* @template T - The tag representing the service (ClassTag or ValueTag)
|
|
1206
|
+
* @template TParams - Optional parameters for service configuration
|
|
1207
|
+
* @param serviceClass - The tag (ClassTag or ValueTag)
|
|
1208
|
+
* @param factory - Factory function for service instantiation with container and optional params
|
|
1209
|
+
* @returns A factory function that creates a service layer
|
|
1210
|
+
*
|
|
1211
|
+
* @example Simple service without dependencies
|
|
1212
|
+
* ```typescript
|
|
1213
|
+
* class LoggerService extends Tag.Class('LoggerService') {
|
|
1214
|
+
* log(message: string) { console.log(message); }
|
|
1215
|
+
* }
|
|
1216
|
+
*
|
|
1217
|
+
* const loggerService = service(LoggerService, () => new LoggerService());
|
|
1218
|
+
* ```
|
|
1219
|
+
*
|
|
1220
|
+
* @example Service with dependencies
|
|
1221
|
+
* ```typescript
|
|
1222
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
1223
|
+
* query() { return []; }
|
|
1224
|
+
* }
|
|
1225
|
+
*
|
|
1226
|
+
* class UserService extends Tag.Class('UserService') {
|
|
1227
|
+
* constructor(private db: DatabaseService) {
|
|
1228
|
+
* super();
|
|
1229
|
+
* }
|
|
1230
|
+
*
|
|
1231
|
+
* getUsers() { return this.db.query(); }
|
|
1232
|
+
* }
|
|
1233
|
+
*
|
|
1234
|
+
* const userService = service(UserService, async (container) =>
|
|
1235
|
+
* new UserService(await container.get(DatabaseService))
|
|
1236
|
+
* );
|
|
1237
|
+
* ```
|
|
1238
|
+
*
|
|
1239
|
+
* @example Service with configuration parameters
|
|
1240
|
+
* ```typescript
|
|
1241
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
1242
|
+
* constructor(private config: { dbUrl: string }) {
|
|
1243
|
+
* super();
|
|
1244
|
+
* }
|
|
1245
|
+
* }
|
|
1246
|
+
*
|
|
1247
|
+
* const dbService = service(
|
|
1248
|
+
* DatabaseService,
|
|
1249
|
+
* (container, params: { dbUrl: string }) => new DatabaseService(params)
|
|
1250
|
+
* );
|
|
1251
|
+
* ```
|
|
1252
|
+
*/
|
|
1253
|
+
declare function service<T extends AnyTag, TParams = undefined>(serviceClass: T, factory: (container: IContainer<ServiceDependencies<T>>, params: TParams) => PromiseOrValue<ServiceOf<T>>): TParams extends undefined ? () => Service<T> : (params: TParams) => Service<T>;
|
|
1254
|
+
//#endregion
|
|
1255
|
+
export { type Inject, Layer, type Scope, type Service, type ServiceOf, Tag, type TaggedClass, type ValueTag, container, layer, scopedContainer, service };
|