sandly 1.0.1 → 2.0.1

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.
Files changed (4) hide show
  1. package/README.md +331 -2609
  2. package/dist/index.d.ts +654 -1554
  3. package/dist/index.js +522 -921
  4. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,1510 +1,894 @@
1
1
  //#region src/tag.d.ts
2
2
  /**
3
3
  * Type representing a tag identifier (string or symbol).
4
- * @internal
5
4
  */
6
5
  type TagId = string | symbol;
7
6
  /**
8
- * Symbol used to identify tagged types within the dependency injection system.
9
- * This symbol is used as a property key to attach metadata to both value tags and service tags.
10
- *
11
- * Note: We can't use a symbol here becuase it produced the following TS error:
12
- * error TS4020: 'extends' clause of exported class 'NotificationService' has or is using private name 'TagIdKey'.
13
- *
7
+ * Symbol used to identify ValueTag objects at runtime.
14
8
  * @internal
15
9
  */
16
10
  declare const ValueTagIdKey = "sandly/ValueTagIdKey";
17
- declare const ServiceTagIdKey = "sandly/ServiceTagIdKey";
18
11
  /**
19
- * Internal string used to identify the type of a tagged type within the dependency injection system.
20
- * This string is used as a property key to attach metadata to both value tags and service tags.
21
- * It is used to carry the type of the tagged type and should not be used directly.
12
+ * Symbol used to carry the phantom type for ValueTag.
22
13
  * @internal
23
14
  */
24
15
  declare const TagTypeKey = "sandly/TagTypeKey";
25
16
  /**
26
- * Type representing a value-based dependency tag.
17
+ * A ServiceTag is any class constructor.
27
18
  *
28
- * Value tags are used to represent non-class dependencies like configuration objects,
29
- * strings, numbers, or any other values. They use phantom types to maintain type safety
30
- * while being distinguishable at runtime through their unique identifiers.
19
+ * Any class can be used directly as a dependency tag without special markers.
31
20
  *
32
- * @template T - The type of the value this tag represents
33
- * @template Id - The unique identifier for this tag (string or symbol)
21
+ * @template T - The type of instances created by this class
34
22
  *
35
23
  * @example
36
24
  * ```typescript
37
- * // Creates a value tag for string configuration
38
- * const ApiKeyTag: ValueTag<'apiKey', string> = Tag.of('apiKey')<string>();
25
+ * class UserService {
26
+ * constructor(private db: Database) {}
27
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
28
+ * }
39
29
  *
40
- * // Register in container
41
- * container.register(ApiKeyTag, () => 'my-secret-key');
30
+ * const container = Container.builder()
31
+ * .add(UserService, ...)
32
+ * .build();
42
33
  * ```
43
34
  */
44
- interface ValueTag<Id extends TagId, T> {
45
- readonly [ValueTagIdKey]: Id;
46
- readonly [TagTypeKey]: T;
47
- }
35
+ type ServiceTag<T = unknown> = new (...args: any[]) => T;
48
36
  /**
49
- * Type representing a class-based dependency tag.
37
+ * A ValueTag represents a non-class dependency (primitives, objects, functions).
50
38
  *
51
- * Tagged classes are created by Tag.Service() and serve as both the dependency identifier
52
- * and the constructor for the service. They extend regular classes with tag metadata
53
- * that the DI system uses for identification and type safety.
39
+ * ValueTags use phantom types to maintain type safety while being
40
+ * distinguishable at runtime through their unique identifiers.
54
41
  *
55
42
  * @template Id - The unique identifier for this tag (string or symbol)
56
- * @template T - The type of instances created by this tagged class
43
+ * @template T - The type of the value this tag represents
57
44
  *
58
45
  * @example
59
46
  * ```typescript
60
- * // Creates a tagged class
61
- * class UserService extends Tag.Service('UserService') {
62
- * getUsers() { return []; }
63
- * }
47
+ * const ApiKeyTag = Tag.of('ApiKey')<string>();
48
+ * const ConfigTag = Tag.of('Config')<{ dbUrl: string }>();
64
49
  *
65
- * // Register in container
66
- * container.register(UserService, () => new UserService());
50
+ * const container = Container.builder()
51
+ * .add(ApiKeyTag, () => process.env.API_KEY!)
52
+ * .add(ConfigTag, () => ({ dbUrl: 'postgres://...' }))
53
+ * .build();
67
54
  * ```
68
- *
69
- * @internal - Users should use Tag.Service() instead of working with this type directly
70
55
  */
71
- interface ServiceTag<Id extends TagId, T> {
72
- new (...args: any[]): T & {
73
- readonly [ServiceTagIdKey]?: Id;
74
- };
75
- readonly [ServiceTagIdKey]?: Id;
56
+ interface ValueTag<Id extends TagId, T> {
57
+ readonly [ValueTagIdKey]: Id;
58
+ readonly [TagTypeKey]: T;
76
59
  }
77
60
  /**
78
- * Utility type that extracts the service type from any dependency tag.
79
- *
80
- * This type is essential for type inference throughout the DI system, allowing
81
- * the container and layers to automatically determine what type of service
82
- * a given tag represents without manual type annotations.
83
- *
84
- * @template T - Any dependency tag (ValueTag or ServiceTag)
85
- * @returns The service type that the tag represents
86
- *
87
- * @example With value tags
88
- * ```typescript
89
- * const StringTag = Tag.of('myString')<string>();
90
- * const ConfigTag = Tag.of('config')<{ apiKey: string }>();
91
- *
92
- * type StringService = TagType<typeof StringTag>; // string
93
- * type ConfigService = TagType<typeof ConfigTag>; // { apiKey: string }
94
- * ```
95
- *
96
- * @example With service tags
97
- * ```typescript
98
- * class UserService extends Tag.Service('UserService') {
99
- * getUsers() { return []; }
100
- * }
101
- *
102
- * type UserServiceType = TagType<typeof UserService>; // UserService
103
- * ```
104
- *
105
- * @example Used in container methods
106
- * ```typescript
107
- * // The container uses TagType internally for type inference
108
- * container.register(StringTag, () => 'hello'); // Factory must return string
109
- * container.register(UserService, () => new UserService()); // Factory must return UserService
61
+ * Union type representing any valid dependency tag in the system.
110
62
  *
111
- * const str: string = await container.resolve(StringTag); // Automatically typed as string
112
- * const user: UserService = await container.resolve(UserService); // Automatically typed as UserService
113
- * ```
63
+ * A tag can be either:
64
+ * - A class constructor (ServiceTag) - for class-based dependencies
65
+ * - A ValueTag - for non-class dependencies (primitives, objects, functions)
114
66
  */
115
- type TagType<TTag extends AnyTag> = TTag extends ValueTag<any, infer T> ? T : TTag extends ServiceTag<any, infer T> ? T : never;
67
+ type AnyTag = ServiceTag | ValueTag<TagId, any>;
116
68
  /**
117
- * Union type representing any valid dependency tag in the system.
69
+ * Extracts the instance/value type from any dependency tag.
118
70
  *
119
- * A tag can be either a value tag (for non-class dependencies) or a tagged class
120
- * (for service classes). This type is used throughout the DI system to constrain
121
- * what can be used as a dependency identifier.
71
+ * - For ServiceTag (class): extracts the instance type
72
+ * - For ValueTag: extracts the value type
122
73
  *
123
- * @example Value tag
74
+ * @example
124
75
  * ```typescript
125
- * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
126
- * // ConfigTag satisfies AnyTag
127
- * ```
76
+ * class UserService { ... }
77
+ * const ConfigTag = Tag.of('Config')<{ url: string }>();
128
78
  *
129
- * @example Class tag
130
- * ```typescript
131
- * class DatabaseService extends Tag.Service('DatabaseService') {}
132
- * // DatabaseService satisfies AnyTag
79
+ * type A = TagType<typeof UserService>; // UserService
80
+ * type B = TagType<typeof ConfigTag>; // { url: string }
133
81
  * ```
134
82
  */
135
- type AnyTag = ValueTag<TagId, any> | ServiceTag<TagId, any>;
83
+ type TagType<T extends AnyTag> = T extends (new (...args: any[]) => infer Instance) ? Instance : T extends ValueTag<any, infer Value> ? Value : never;
136
84
  /**
137
- * Utility object containing factory functions for creating dependency tags.
138
- *
139
- * The Tag object provides the primary API for creating both value tags and service tags
140
- * used throughout the dependency injection system. It's the main entry point for
141
- * defining dependencies in a type-safe way.
85
+ * Utility object for creating and working with tags.
142
86
  */
143
87
  declare const Tag: {
144
88
  /**
145
- * Creates a value tag factory for dependencies that are not classes.
89
+ * Creates a ValueTag factory for non-class dependencies.
146
90
  *
147
- * This method returns a factory function that, when called with a type parameter,
148
- * creates a value tag for that type. The tag has a string or symbol-based identifier
149
- * that must be unique within your application.
91
+ * @param id - The unique identifier for this tag (string or symbol)
92
+ * @returns A factory function that creates a ValueTag for the specified type
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const ApiKeyTag = Tag.of('ApiKey')<string>();
97
+ * const PortTag = Tag.of('Port')<number>();
98
+ * const ConfigTag = Tag.of('Config')<{ dbUrl: string; port: number }>();
99
+ * ```
100
+ */
101
+ of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
102
+ /**
103
+ * Gets a string identifier for any tag, used for error messages.
150
104
  *
151
- * @template Id - The string or symbol identifier for this tag (must be unique)
152
- * @param id - The unique string or symbol identifier for this tag
153
- * @returns A factory function that creates value tags for the specified type
105
+ * For classes: uses static `Tag` property if present, otherwise `constructor.name`
106
+ * For ValueTags: uses the tag's id
154
107
  *
155
- * @example Basic usage with strings
108
+ * @example
156
109
  * ```typescript
157
- * const ApiKeyTag = Tag.of('apiKey')<string>();
158
- * const ConfigTag = Tag.of('config')<{ dbUrl: string; port: number }>();
110
+ * class UserService {}
111
+ * Tag.id(UserService); // "UserService"
112
+ *
113
+ * class ApiClient {
114
+ * static readonly Tag = 'MyApiClient'; // Custom name
115
+ * }
116
+ * Tag.id(ApiClient); // "MyApiClient"
159
117
  *
160
- * container
161
- * .register(ApiKeyTag, () => process.env.API_KEY!)
162
- * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost', port: 5432 }));
118
+ * const ConfigTag = Tag.of('app.config')<Config>();
119
+ * Tag.id(ConfigTag); // "app.config"
163
120
  * ```
121
+ */
122
+ id: (tag: AnyTag) => string;
123
+ /**
124
+ * Type guard to check if a value is a ServiceTag (class constructor).
164
125
  *
165
- * @example Usage with symbols
166
- * ```typescript
167
- * const DB_CONFIG_SYM = Symbol('database-config');
168
- * const ConfigTag = Tag.of(DB_CONFIG_SYM)<DatabaseConfig>();
126
+ * Returns true for class declarations, class expressions, and regular functions
127
+ * (which can be used as constructors in JavaScript).
169
128
  *
170
- * container.register(ConfigTag, () => ({ host: 'localhost', port: 5432 }));
129
+ * Returns false for arrow functions since they cannot be used with `new`.
130
+ */
131
+ isServiceTag: (x: unknown) => x is ServiceTag;
132
+ /**
133
+ * Type guard to check if a value is a ValueTag.
134
+ */
135
+ isValueTag: (x: unknown) => x is ValueTag<TagId, any>;
136
+ /**
137
+ * Type guard to check if a value is any kind of tag (ServiceTag or ValueTag).
138
+ */
139
+ isTag: (x: unknown) => x is AnyTag;
140
+ }; //#endregion
141
+ //#region src/types.d.ts
142
+ /**
143
+ * Type representing a value that can be either synchronous or a Promise.
144
+ * Used throughout the DI system to support both sync and async factories/finalizers.
145
+ */
146
+ type PromiseOrValue<T> = T | Promise<T>;
147
+ /**
148
+ * Variance marker types for type-level programming.
149
+ * Used to control how generic types behave with respect to subtyping.
150
+ */
151
+ type Contravariant<A> = (_: A) => void;
152
+ type Covariant<A> = (_: never) => A;
153
+
154
+ //#endregion
155
+ //#region src/layer.d.ts
156
+ /**
157
+ * Replaces the TTags type parameter in a builder type with a new type.
158
+ * Preserves the concrete builder type (ContainerBuilder or ScopedContainerBuilder).
159
+ * @internal
160
+ */
161
+ type WithBuilderTags<TBuilder, TNewTags extends AnyTag> = TBuilder extends ScopedContainerBuilder ? ScopedContainerBuilder<TNewTags> : TBuilder extends ContainerBuilder ? ContainerBuilder<TNewTags> : IContainerBuilder<TNewTags>;
162
+ /**
163
+ * Defines what constitutes a valid dependency for a given parameter type T.
164
+ * A valid dependency is either:
165
+ * - A ServiceTag (class) whose instances are assignable to T
166
+ * - A ValueTag whose value type is assignable to T
167
+ * - A raw value of type T
168
+ * @internal
169
+ */
170
+ type ValidDepFor<T> = (T extends object ? ServiceTag<T> | ValueTag<any, T> : ValueTag<any, T>) | T;
171
+ /**
172
+ * Maps constructor parameters to valid dependency types.
173
+ * Each parameter type T becomes ValidDepFor<T>.
174
+ * @internal
175
+ */
176
+ type ValidDepsFor<TParams extends readonly unknown[]> = { readonly [K in keyof TParams]: ValidDepFor<TParams[K]> };
177
+ /**
178
+ * Extracts only the tags from a dependency array (filters out raw values).
179
+ * Used to determine layer requirements.
180
+ * @internal
181
+ */
182
+ type ExtractTags<T extends readonly unknown[]> = { [K in keyof T]: T[K] extends AnyTag ? T[K] : never }[number];
183
+ /**
184
+ * The most generic layer type that accepts any concrete layer.
185
+ */
186
+ type AnyLayer = Layer<any, any>;
187
+ /**
188
+ * The type ID for the Layer interface.
189
+ */
190
+ declare const LayerTypeId: unique symbol;
191
+ /**
192
+ * Helper type that extracts the union of all requirements from an array of layers.
193
+ * @internal
194
+ */
195
+ type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
196
+ /**
197
+ * Helper type that extracts the union of all provisions from an array of layers.
198
+ * @internal
199
+ */
200
+ type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
201
+ /**
202
+ * A dependency layer represents a reusable, composable unit of dependency registrations.
203
+ * Layers allow you to organize your dependency injection setup into logical groups
204
+ * that can be combined and reused across different contexts.
205
+ *
206
+ * ## Type Variance
207
+ *
208
+ * - **TRequires (covariant)**: A layer requiring fewer dependencies can substitute one requiring more
209
+ * - **TProvides (contravariant)**: A layer providing more services can substitute one providing fewer
210
+ *
211
+ * @template TRequires - The union of tags this layer requires
212
+ * @template TProvides - The union of tags this layer provides
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * // Create layers using Layer.service(), Layer.value(), or Layer.create()
217
+ * const dbLayer = Layer.service(Database, []);
218
+ * const userLayer = Layer.service(UserService, [Database]);
219
+ *
220
+ * // Compose layers
221
+ * const appLayer = userLayer.provide(dbLayer);
222
+ *
223
+ * // Create container from layer
224
+ * const container = Container.from(appLayer);
225
+ * ```
226
+ */
227
+ interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
228
+ readonly [LayerTypeId]?: {
229
+ readonly _TRequires: Covariant<TRequires>;
230
+ readonly _TProvides: Contravariant<TProvides>;
231
+ };
232
+ /**
233
+ * Applies this layer's registrations to a container builder.
234
+ * Works with both ContainerBuilder and ScopedContainerBuilder.
235
+ * @internal
236
+ */
237
+ apply: <TBuilder extends IContainerBuilder<TRequires>>(builder: TBuilder) => WithBuilderTags<TBuilder, TRequires | TProvides>;
238
+ /**
239
+ * Provides a dependency layer to this layer, creating a pipeline.
240
+ * The result only exposes this layer's provisions (not the dependency's).
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * const appLayer = apiLayer
245
+ * .provide(serviceLayer)
246
+ * .provide(databaseLayer);
171
247
  * ```
248
+ */
249
+ provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
250
+ /**
251
+ * Provides a dependency layer and merges both layers' provisions.
252
+ * Unlike `.provide()`, this exposes both this layer's and the dependency's provisions.
172
253
  *
173
- * @example Primitive values
254
+ * @example
174
255
  * ```typescript
175
- * const PortTag = Tag.of('port')<number>();
176
- * const EnabledTag = Tag.of('enabled')<boolean>();
256
+ * const infraLayer = dbLayer.provideMerge(configLayer);
257
+ * // Provides both Database and Config
258
+ * ```
259
+ */
260
+ provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
261
+ /**
262
+ * Merges this layer with another independent layer.
263
+ * Combines their requirements and provisions.
177
264
  *
178
- * container
179
- * .register(PortTag, () => 3000)
180
- * .register(EnabledTag, () => true);
265
+ * @example
266
+ * ```typescript
267
+ * const infraLayer = persistenceLayer.merge(loggingLayer);
181
268
  * ```
269
+ */
270
+ merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
271
+ }
272
+ /**
273
+ * Consolidated Layer API for creating and composing dependency layers.
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * // Define services
278
+ * class Database {
279
+ * query(sql: string) { return []; }
280
+ * }
281
+ *
282
+ * class UserService {
283
+ * constructor(private db: Database) {}
284
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
285
+ * }
286
+ *
287
+ * // Create layers
288
+ * const dbLayer = Layer.service(Database, []);
289
+ * const userLayer = Layer.service(UserService, [Database]);
290
+ *
291
+ * // Compose and create container
292
+ * const appLayer = userLayer.provide(dbLayer);
293
+ * const container = Container.from(appLayer);
294
+ *
295
+ * const users = await container.resolve(UserService);
296
+ * ```
297
+ */
298
+ declare const Layer: {
299
+ /**
300
+ * Creates a layer that provides a class service with automatic dependency injection.
301
+ *
302
+ * The dependencies array must match the constructor parameters exactly (order and types).
303
+ * This is validated at compile time.
304
+ *
305
+ * @param cls - The service class
306
+ * @param deps - Array of dependencies (tags or raw values) matching constructor params
307
+ * @param options - Optional cleanup function for the service
182
308
  *
183
- * @example Complex objects
309
+ * @example
184
310
  * ```typescript
185
- * interface DatabaseConfig {
186
- * host: string;
187
- * port: number;
188
- * database: string;
311
+ * class UserService {
312
+ * constructor(private db: Database, private apiKey: string) {}
189
313
  * }
190
314
  *
191
- * const DbConfigTag = Tag.of('database-config')<DatabaseConfig>();
192
- * container.register(DbConfigTag, () => ({
193
- * host: 'localhost',
194
- * port: 5432,
195
- * database: 'myapp'
196
- * }));
315
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
316
+ *
317
+ * // Dependencies must match constructor: (Database, string)
318
+ * const userLayer = Layer.service(UserService, [Database, ApiKeyTag]);
319
+ *
320
+ * // Also works with raw values
321
+ * const userLayer2 = Layer.service(UserService, [Database, 'my-api-key']);
197
322
  * ```
198
323
  */
199
- of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
324
+ service<TClass extends ServiceTag, const TDeps extends readonly unknown[]>(cls: TClass, deps: TDeps & ValidDepsFor<ConstructorParameters<TClass>>, options?: {
325
+ cleanup?: Finalizer<InstanceType<TClass>>;
326
+ }): Layer<ExtractTags<TDeps>, TClass>;
200
327
  /**
201
- * Creates a base class that can be extended to create service classes with dependency tags.
328
+ * Creates a layer that provides a constant value or pre-instantiated instance.
202
329
  *
203
- * This is the primary way to define service classes in the dependency injection system.
204
- * Classes that extend the returned base class become both the dependency identifier
205
- * and the implementation, providing type safety and clear semantics.
330
+ * Works with both ValueTags (for constants) and ServiceTags (for pre-instantiated instances, useful in tests).
206
331
  *
207
- * @template Id - The unique identifier for this service class
208
- * @param id - The unique identifier (string or symbol) for this service
209
- * @returns A base class that can be extended to create tagged service classes
332
+ * @param tag - The tag (ValueTag or ServiceTag) to register
333
+ * @param value - The value or instance to provide
210
334
  *
211
- * @example Basic service class
335
+ * @example ValueTag (constant)
212
336
  * ```typescript
213
- * class UserService extends Tag.Service('UserService') {
214
- * getUsers() {
215
- * return ['alice', 'bob'];
216
- * }
217
- * }
337
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
338
+ * const ConfigTag = Tag.of('config')<{ port: number }>();
218
339
  *
219
- * container.register(UserService, () => new UserService());
340
+ * const configLayer = Layer.value(ApiKeyTag, 'secret-key')
341
+ * .merge(Layer.value(ConfigTag, { port: 3000 }));
220
342
  * ```
221
343
  *
222
- * @example Service with dependencies
344
+ * @example ServiceTag (pre-instantiated instance, useful for testing)
223
345
  * ```typescript
224
- * class DatabaseService extends Tag.Service('DatabaseService') {
225
- * query(sql: string) { return []; }
346
+ * class UserService {
347
+ * getUsers() { return []; }
226
348
  * }
227
349
  *
228
- * class UserRepository extends Tag.Service('UserRepository') {
229
- * constructor(private db: DatabaseService) {
230
- * super();
231
- * }
350
+ * const mockUserService = new UserService();
351
+ * const testLayer = Layer.value(UserService, mockUserService);
352
+ * ```
353
+ */
354
+ value<T extends AnyTag>(tag: T, value: TagType<T>): Layer<never, T>;
355
+ /**
356
+ * Creates a custom layer with full control over the factory logic.
357
+ *
358
+ * Use this when you need custom instantiation logic that can't be expressed
359
+ * with `Layer.service()` or `Layer.value()`.
232
360
  *
233
- * findUser(id: string) {
234
- * return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
235
- * }
236
- * }
361
+ * - `TRequires` is inferred from the `requires` array
362
+ * - `TProvides` is inferred from what `apply` adds to the builder
237
363
  *
238
- * container
239
- * .register(DatabaseService, () => new DatabaseService())
240
- * .register(UserRepository, async (ctx) =>
241
- * new UserRepository(await ctx.resolve(DatabaseService))
242
- * );
243
- * ```
364
+ * @param options.requires - Array of tags this layer requires (use [] for no requirements)
365
+ * @param options.apply - Function that adds registrations to a builder
244
366
  *
245
- * @example With symbol identifiers
367
+ * @example
246
368
  * ```typescript
247
- * const ServiceId = Symbol('InternalService');
369
+ * // Layer with dependencies - TProvides inferred from builder.add()
370
+ * const cacheLayer = Layer.create({
371
+ * requires: [Database],
372
+ * apply: (builder) => builder
373
+ * .add(Cache, async (ctx) => {
374
+ * const db = await ctx.resolve(Database);
375
+ * return new Cache(db, { ttl: 3600 });
376
+ * })
377
+ * });
378
+ * // Type: Layer<typeof Database, typeof Cache>
248
379
  *
249
- * class InternalService extends Tag.Service(ServiceId) {
250
- * doInternalWork() { return 'work'; }
251
- * }
380
+ * // Layer with no dependencies
381
+ * const dbLayer = Layer.create({
382
+ * requires: [],
383
+ * apply: (builder) => builder.add(Database, () => new Database())
384
+ * });
385
+ * // Type: Layer<never, typeof Database>
252
386
  * ```
253
387
  */
254
- Service: <Id extends TagId>(id: Id) => ServiceTag<Id, {
255
- readonly "sandly/ServiceTagIdKey"?: Id;
256
- }>;
388
+ create<const TRequires extends readonly AnyTag[], TAllTags extends AnyTag>(options: {
389
+ requires: TRequires;
390
+ apply: (builder: IContainerBuilder<TRequires[number]>) => IContainerBuilder<TAllTags>;
391
+ }): Layer<TRequires[number], Exclude<TAllTags, TRequires[number]>>;
257
392
  /**
258
- * Extracts the string representation of a tag's identifier.
393
+ * Creates an empty layer with no requirements or provisions.
259
394
  *
260
- * This utility function returns a human-readable string for any tag's identifier,
261
- * whether it's a string-based or symbol-based tag. Primarily used internally
262
- * for error messages and debugging.
395
+ * @example
396
+ * ```typescript
397
+ * const baseLayer = Layer.empty()
398
+ * .merge(configLayer)
399
+ * .merge(serviceLayer);
400
+ * ```
401
+ */
402
+ empty(): Layer<never, never>;
403
+ /**
404
+ * Merges multiple layers at once.
263
405
  *
264
- * @param tag - Any valid dependency tag (value tag or service tag)
265
- * @returns String representation of the tag's identifier
406
+ * @param layers - At least 2 layers to merge
407
+ * @returns A layer combining all requirements and provisions
266
408
  *
267
409
  * @example
268
410
  * ```typescript
269
- * const StringTag = Tag.of('myString')<string>();
270
- * class ServiceClass extends Tag.Service('MyService') {}
271
- *
272
- * console.log(Tag.id(StringTag)); // "myString"
273
- * console.log(Tag.id(ServiceClass)); // "MyService"
411
+ * const infraLayer = Layer.mergeAll(
412
+ * persistenceLayer,
413
+ * messagingLayer,
414
+ * observabilityLayer
415
+ * );
274
416
  * ```
275
- *
276
- * @internal - Primarily for internal use in error messages and debugging
277
417
  */
278
- id: (tag: AnyTag) => TagId | undefined;
279
- isTag: (tag: unknown) => tag is AnyTag;
418
+ mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
419
+ /**
420
+ * Merges exactly two layers.
421
+ * Equivalent to `layer1.merge(layer2)`.
422
+ */
423
+ merge<TRequires1 extends AnyTag, TProvides1 extends AnyTag, TRequires2 extends AnyTag, TProvides2 extends AnyTag>(layer1: Layer<TRequires1, TProvides1>, layer2: Layer<TRequires2, TProvides2>): Layer<TRequires1 | TRequires2, TProvides1 | TProvides2>;
280
424
  };
281
- /**
282
- * String used to store the original ValueTag in Inject<T> types.
283
- * This prevents property name collisions while allowing type-level extraction.
284
- */
285
- declare const InjectSource = "sandly/InjectSource";
286
- /**
287
- * Helper type for injecting ValueTag dependencies in constructor parameters.
288
- * This allows clean specification of ValueTag dependencies while preserving
289
- * the original tag information for dependency inference.
290
- *
291
- * The phantom property is optional to allow normal runtime values to be assignable.
292
- *
293
- * @template T - A ValueTag type
294
- * @returns The value type with optional phantom tag metadata for dependency inference
295
- *
296
- * @example
297
- * ```typescript
298
- * const ApiKeyTag = Tag.of('apiKey')<string>();
299
- *
300
- * class UserService extends Tag.Service('UserService') {
301
- * constructor(
302
- * private db: DatabaseService, // ServiceTag - works automatically
303
- * private apiKey: Inject<typeof ApiKeyTag> // ValueTag - type is string, tag preserved
304
- * ) {
305
- * super();
306
- * }
307
- * }
308
- * ```
309
- */
310
- type Inject<T extends ValueTag<TagId, unknown>> = T extends ValueTag<any, infer V> ? V & {
311
- readonly [InjectSource]?: T;
312
- } : never;
313
- /**
314
- * Helper type to extract the original ValueTag from an Inject<T> type.
315
- * Since InjectSource is optional, we need to check for both presence and absence.
316
- * @internal
317
- */
318
- type ExtractInjectTag<T> = T extends {
319
- readonly [InjectSource]?: infer U;
320
- } ? U : never; //#endregion
321
- //#region src/types.d.ts
322
- type PromiseOrValue<T> = T | Promise<T>;
323
- type Contravariant<A> = (_: A) => void;
324
- type Covariant<A> = (_: never) => A;
325
425
 
326
426
  //#endregion
327
427
  //#region src/container.d.ts
328
428
  /**
329
- * Type representing a factory function used to create dependency instances.
330
- *
331
- * Factory functions are the core mechanism for dependency creation in the DI system.
332
- * They receive a dependency container and can use it to resolve other dependencies
333
- * that the service being created needs.
429
+ * Factory function that creates a dependency instance.
334
430
  *
335
- * The factory can be either synchronous (returning T directly) or asynchronous
336
- * (returning Promise<T>). The container handles both cases transparently.
431
+ * Receives a resolution context for injecting other dependencies.
432
+ * Can be synchronous or asynchronous.
337
433
  *
338
434
  * @template T - The type of the service instance being created
339
- * @template TRequires - Union type of all required dependencies
340
- *
341
- * @example Synchronous factory
342
- * ```typescript
343
- * const factory: Factory<DatabaseService, never> = (ctx) => {
344
- * return new DatabaseService('sqlite://memory');
345
- * };
346
- * ```
347
- *
348
- * @example Asynchronous factory with dependencies
349
- * ```typescript
350
- * const factory: Factory<UserService, typeof ConfigTag | typeof DatabaseService> = async (ctx) => {
351
- * const [config, db] = await Promise.all([
352
- * ctx.resolve(ConfigTag),
353
- * ctx.resolve(DatabaseService)
354
- * ]);
355
- * return new UserService(config, db);
356
- * };
357
- * ```
435
+ * @template TRequires - Union type of required dependencies
358
436
  */
359
437
  type Factory<T, TRequires extends AnyTag> = (ctx: ResolutionContext<TRequires>) => PromiseOrValue<T>;
360
438
  /**
361
- * Type representing a finalizer function used to clean up dependency instances.
439
+ * Cleanup function called when the container is destroyed.
362
440
  *
363
- * Finalizers are optional cleanup functions that are called when the container
364
- * is destroyed via `container.destroy()`. They receive the created instance
365
- * and should perform any necessary cleanup (closing connections, releasing resources, etc.).
366
- *
367
- * Like factories, finalizers can be either synchronous or asynchronous.
368
- * All finalizers are called concurrently during container destruction.
369
- *
370
- * @template T - The type of the service instance being finalized
371
- *
372
- * @example Synchronous finalizer
373
- * ```typescript
374
- * const cleanup: Finalizer<FileHandle> = (fileHandle) => {
375
- * fileHandle.close();
376
- * };
377
- * ```
378
- *
379
- * @example Asynchronous finalizer
380
- * ```typescript
381
- * const cleanup: Finalizer<DatabaseConnection> = async (connection) => {
382
- * await connection.disconnect();
383
- * };
384
- * ```
385
- *
386
- * @example Resilient finalizer
387
- * ```typescript
388
- * const cleanup: Finalizer<HttpServer> = async (server) => {
389
- * try {
390
- * await server.close();
391
- * } catch (error) {
392
- * if (!error.message.includes('already closed')) {
393
- * throw error; // Re-throw unexpected errors
394
- * }
395
- * // Ignore "already closed" errors
396
- * }
397
- * };
398
- * ```
441
+ * @template T - The type of the service instance being cleaned up
399
442
  */
400
443
  type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
401
444
  /**
402
- * Type representing a complete dependency lifecycle with both factory and finalizer.
445
+ * Complete dependency lifecycle with factory and optional cleanup.
403
446
  *
404
- * This interface is used when registering dependencies that need cleanup. Instead of
405
- * passing separate factory and finalizer parameters, you can pass an object
406
- * containing both.
407
- *
408
- * Since this is an interface, you can also implement it as a class for better
409
- * organization and reuse. This is particularly useful when you have complex
410
- * lifecycle logic or want to share lifecycle definitions across multiple services.
447
+ * Can be implemented as a class for complex lifecycle logic.
411
448
  *
412
449
  * @template T - The instance type
413
- * @template TRequires - Union type of all required dependencies
414
- *
415
- * @example Using DependencyLifecycle as an object
416
- * ```typescript
417
- * import { Container, Tag } from 'sandly';
418
- *
419
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
420
- * async connect() { return; }
421
- * async disconnect() { return; }
422
- * }
423
- *
424
- * const lifecycle: DependencyLifecycle<DatabaseConnection, never> = {
425
- * create: async () => {
426
- * const conn = new DatabaseConnection();
427
- * await conn.connect();
428
- * return conn;
429
- * },
430
- * cleanup: async (conn) => {
431
- * await conn.disconnect();
432
- * }
433
- * };
434
- *
435
- * Container.empty().register(DatabaseConnection, lifecycle);
436
- * ```
437
- *
438
- * @example Implementing DependencyLifecycle as a class with dependencies
439
- * ```typescript
440
- * import { Container, Tag, type ResolutionContext } from 'sandly';
441
- *
442
- * class Logger extends Tag.Service('Logger') {
443
- * log(message: string) { console.log(message); }
444
- * }
445
- *
446
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
447
- * constructor(private logger: Logger, private url: string) { super(); }
448
- * async connect() { this.logger.log('Connected'); }
449
- * async disconnect() { this.logger.log('Disconnected'); }
450
- * }
451
- *
452
- * class DatabaseLifecycle implements DependencyLifecycle<DatabaseConnection, typeof Logger> {
453
- * constructor(private url: string) {}
454
- *
455
- * async create(ctx: ResolutionContext<typeof Logger>): Promise<DatabaseConnection> {
456
- * const logger = await ctx.resolve(Logger);
457
- * const conn = new DatabaseConnection(logger, this.url);
458
- * await conn.connect();
459
- * return conn;
460
- * }
461
- *
462
- * async cleanup(conn: DatabaseConnection): Promise<void> {
463
- * await conn.disconnect();
464
- * }
465
- * }
466
- *
467
- * const container = Container.empty()
468
- * .register(Logger, () => new Logger())
469
- * .register(DatabaseConnection, new DatabaseLifecycle('postgresql://localhost:5432'));
470
- * ```
471
- *
472
- * @example Class with only factory (no cleanup)
473
- * ```typescript
474
- * import { Container, Tag } from 'sandly';
475
- *
476
- * class SimpleService extends Tag.Service('SimpleService') {}
477
- *
478
- * class SimpleServiceLifecycle implements DependencyLifecycle<SimpleService, never> {
479
- * create(): SimpleService {
480
- * return new SimpleService();
481
- * }
482
- * // cleanup is optional, so it can be omitted
483
- * }
484
- *
485
- * const container = Container.empty().register(
486
- * SimpleService,
487
- * new SimpleServiceLifecycle()
488
- * );
489
- * ```
450
+ * @template TRequires - Union type of required dependencies
490
451
  */
491
452
  interface DependencyLifecycle<T, TRequires extends AnyTag> {
492
453
  create: Factory<T, TRequires>;
493
454
  cleanup?: Finalizer<T>;
494
455
  }
495
456
  /**
496
- * Union type representing all valid dependency registration specifications.
497
- *
498
- * A dependency can be registered either as:
499
- * - A simple factory function that creates the dependency
500
- * - A complete lifecycle object with both factory and finalizer
501
- *
502
- * @template T - The dependency tag type
503
- * @template TRequires - Union type of all required dependencies
504
- *
505
- * @example Simple factory registration
506
- * ```typescript
507
- * const spec: DependencySpec<typeof UserService, never> =
508
- * () => new UserService();
509
- *
510
- * Container.empty().register(UserService, spec);
511
- * ```
512
- *
513
- * @example Lifecycle registration
514
- * ```typescript
515
- * const spec: DependencySpec<typeof DatabaseConnection, never> = {
516
- * create: () => new DatabaseConnection(),
517
- * cleanup: (conn) => conn.close()
518
- * };
519
- *
520
- * Container.empty().register(DatabaseConnection, spec);
521
- * ```
457
+ * Valid dependency registration: either a factory function or lifecycle object.
522
458
  */
523
459
  type DependencySpec<T extends AnyTag, TRequires extends AnyTag> = Factory<TagType<T>, TRequires> | DependencyLifecycle<TagType<T>, TRequires>;
524
460
  /**
525
- * Type representing the context available to factory functions during dependency resolution.
461
+ * Context available to factory functions during resolution.
526
462
  *
527
- * This type contains only the `resolve` and `resolveAll` methods from the container, which are used to retrieve
528
- * other dependencies during the creation of a service.
529
- *
530
- * @template TTags - Union type of all dependencies available in the container
463
+ * Provides `resolve` and `resolveAll` for injecting dependencies.
531
464
  */
532
465
  type ResolutionContext<TTags extends AnyTag> = Pick<IContainer<TTags>, 'resolve' | 'resolveAll'>;
466
+ /**
467
+ * Unique symbol for container type branding.
468
+ */
533
469
  declare const ContainerTypeId: unique symbol;
534
470
  /**
535
- * Interface representing a container that can register and retrieve dependencies.
471
+ * Interface for dependency containers.
536
472
  *
537
- * @template TTags - Union type of all dependencies available in the container
473
+ * @template TTags - Union type of registered dependency tags
538
474
  */
539
475
  interface IContainer<TTags extends AnyTag = never> {
540
476
  readonly [ContainerTypeId]: {
541
477
  readonly _TTags: Contravariant<TTags>;
542
478
  };
543
- register: <T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>) => IContainer<TTags | T>;
544
- has(tag: AnyTag): boolean;
545
- exists(tag: AnyTag): boolean;
546
479
  resolve: <T extends TTags>(tag: T) => Promise<TagType<T>>;
547
480
  resolveAll: <const T extends readonly TTags[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
481
+ use: <T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>) => Promise<R>;
548
482
  destroy(): Promise<void>;
549
483
  }
550
484
  /**
551
- * Extracts the registered tags (TTags) from a container type.
485
+ * Extracts the registered tags from a container type.
552
486
  */
553
487
  type ContainerTags<C> = C extends IContainer<infer TTags> ? TTags : never;
554
488
  /**
555
- * A type-safe dependency injection container that manages service instantiation,
556
- * caching, and lifecycle management with support for async dependencies and
557
- * circular dependency detection.
489
+ * Common interface for container builders that support adding dependencies.
490
+ * Used by Layer to work with both ContainerBuilder and ScopedContainerBuilder.
491
+ */
492
+ interface IContainerBuilder<TTags extends AnyTag = never> {
493
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): IContainerBuilder<TTags | T>;
494
+ }
495
+ /**
496
+ * Extracts the registered tags from a builder type.
497
+ */
498
+ type BuilderTags<B> = B extends IContainerBuilder<infer TTags> ? TTags : never;
499
+ /**
500
+ * Builder for constructing immutable containers.
558
501
  *
559
- * The container maintains complete type safety by tracking registered dependencies
560
- * at the type level, ensuring that only registered dependencies can be retrieved
561
- * and preventing runtime errors.
502
+ * Use `Container.builder()` to create a builder, then chain `.add()` calls
503
+ * to register dependencies, and finally call `.build()` to create the container.
562
504
  *
563
- * @template TTags - Union type of all registered dependency tags in this container
505
+ * @template TTags - Union type of registered dependency tags
564
506
  *
565
- * @example Basic usage with service tags
507
+ * @example
566
508
  * ```typescript
567
- * import { container, Tag } from 'sandly';
568
- *
569
- * class DatabaseService extends Tag.Service('DatabaseService') {
570
- * query() { return 'data'; }
571
- * }
572
- *
573
- * class UserService extends Tag.Service('UserService') {
574
- * constructor(private db: DatabaseService) {}
575
- * getUser() { return this.db.query(); }
576
- * }
577
- *
578
- * const container = Container.empty()
579
- * .register(DatabaseService, () => new DatabaseService())
580
- * .register(UserService, async (ctx) =>
581
- * new UserService(await ctx.resolve(DatabaseService))
582
- * );
583
- *
584
- * const userService = await c.resolve(UserService);
509
+ * const container = Container.builder()
510
+ * .add(Database, () => new Database())
511
+ * .add(UserService, async (ctx) =>
512
+ * new UserService(await ctx.resolve(Database))
513
+ * )
514
+ * .build();
585
515
  * ```
516
+ */
517
+ declare class ContainerBuilder<TTags extends AnyTag = never> {
518
+ private readonly factories;
519
+ private readonly finalizers;
520
+ /**
521
+ * Registers a dependency with a factory function or lifecycle object.
522
+ *
523
+ * @param tag - The dependency tag (class or ValueTag)
524
+ * @param spec - Factory function or lifecycle object
525
+ * @returns The builder with updated type information
526
+ */
527
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ContainerBuilder<TTags | T>;
528
+ /**
529
+ * Creates an immutable container from the registered dependencies.
530
+ */
531
+ build(): Container<TTags>;
532
+ }
533
+ /**
534
+ * Type-safe dependency injection container.
586
535
  *
587
- * @example Usage with value tags
588
- * ```typescript
589
- * const ApiKeyTag = Tag.of('apiKey')<string>();
590
- * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
536
+ * Containers are immutable - use `Container.builder()` to create one.
537
+ * Each dependency is created once (singleton) and cached.
591
538
  *
592
- * const container = Container.empty()
593
- * .register(ApiKeyTag, () => process.env.API_KEY!)
594
- * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
539
+ * @template TTags - Union type of registered dependency tags
595
540
  *
596
- * const apiKey = await c.resolve(ApiKeyTag);
597
- * const config = await c.resolve(ConfigTag);
598
- * ```
599
- *
600
- * @example With finalizers for cleanup
541
+ * @example
601
542
  * ```typescript
602
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
603
- * async connect() { return; }
604
- * async disconnect() { return; }
543
+ * class Database {
544
+ * query(sql: string) { return []; }
545
+ * }
546
+ *
547
+ * class UserService {
548
+ * constructor(private db: Database) {}
549
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
605
550
  * }
606
551
  *
607
- * const container = Container.empty().register(
608
- * DatabaseConnection,
609
- * async () => {
610
- * const conn = new DatabaseConnection();
611
- * await conn.connect();
612
- * return conn;
613
- * },
614
- * async (conn) => conn.disconnect() // Finalizer for cleanup
615
- * );
552
+ * const container = Container.builder()
553
+ * .add(Database, () => new Database())
554
+ * .add(UserService, async (ctx) =>
555
+ * new UserService(await ctx.resolve(Database))
556
+ * )
557
+ * .build();
616
558
  *
617
- * // Later...
618
- * await c.destroy(); // Calls all finalizers
559
+ * const userService = await container.resolve(UserService);
619
560
  * ```
620
561
  */
621
- declare class Container<TTags extends AnyTag> implements IContainer<TTags> {
562
+ declare class Container<TTags extends AnyTag = never> implements IContainer<TTags> {
622
563
  readonly [ContainerTypeId]: {
623
564
  readonly _TTags: Contravariant<TTags>;
624
565
  };
625
- protected constructor();
626
566
  /**
627
- * Cache of instantiated dependencies as promises.
628
- * Ensures singleton behavior and supports concurrent access.
567
+ * Cache of instantiated dependencies.
629
568
  * @internal
630
569
  */
631
570
  protected readonly cache: Map<AnyTag, Promise<unknown>>;
632
571
  /**
633
- * Factory functions for creating dependency instances.
572
+ * Factory functions for creating dependencies.
634
573
  * @internal
635
574
  */
636
575
  protected readonly factories: Map<AnyTag, Factory<unknown, TTags>>;
637
576
  /**
638
- * Finalizer functions for cleaning up dependencies when the container is destroyed.
577
+ * Cleanup functions for dependencies.
639
578
  * @internal
640
579
  */
641
580
  protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
642
581
  /**
643
- * Flag indicating whether this container has been destroyed.
582
+ * Whether this container has been destroyed.
644
583
  * @internal
645
584
  */
646
585
  protected isDestroyed: boolean;
647
586
  /**
648
- * Creates a new empty container instance.
649
- * @returns A new empty Container instance with no registered dependencies.
587
+ * @internal - Use Container.builder() or Container.empty()
650
588
  */
651
- static empty(): Container<never>;
589
+ protected constructor(factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
652
590
  /**
653
- * Registers a dependency in the container with a factory function and optional finalizer.
654
- *
655
- * The factory function receives the current container instance and must return the
656
- * service instance (or a Promise of it). The container tracks the registration at
657
- * the type level, ensuring type safety for subsequent `.resolve()` calls.
658
- *
659
- * If a dependency is already registered, this method will override it unless the
660
- * dependency has already been instantiated, in which case it will throw an error.
661
- *
662
- * @template T - The dependency tag being registered
663
- * @param tag - The dependency tag (class or value tag)
664
- * @param factory - Function that creates the service instance, receives container for dependency injection
665
- * @param finalizer - Optional cleanup function called when container is destroyed
666
- * @returns A new container instance with the dependency registered
667
- * @throws {ContainerDestroyedError} If the container has been destroyed
668
- * @throws {Error} If the dependency has already been instantiated
669
- *
670
- * @example Registering a simple service
671
- * ```typescript
672
- * class LoggerService extends Tag.Service('LoggerService') {
673
- * log(message: string) { console.log(message); }
674
- * }
675
- *
676
- * const container = Container.empty().register(
677
- * LoggerService,
678
- * () => new LoggerService()
679
- * );
680
- * ```
591
+ * @internal - Used by ContainerBuilder
592
+ */
593
+ static _createFromBuilder<T extends AnyTag>(factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): Container<T>;
594
+ /**
595
+ * Creates a new container builder.
681
596
  *
682
- * @example Registering with dependencies
597
+ * @example
683
598
  * ```typescript
684
- * class UserService extends Tag.Service('UserService') {
685
- * constructor(private db: DatabaseService, private logger: LoggerService) {}
686
- * }
687
- *
688
- * const container = Container.empty()
689
- * .register(DatabaseService, () => new DatabaseService())
690
- * .register(LoggerService, () => new LoggerService())
691
- * .register(UserService, async (ctx) =>
692
- * new UserService(
693
- * await ctx.resolve(DatabaseService),
694
- * await ctx.resolve(LoggerService)
695
- * )
696
- * );
599
+ * const container = Container.builder()
600
+ * .add(Database, () => new Database())
601
+ * .build();
697
602
  * ```
603
+ */
604
+ static builder(): ContainerBuilder;
605
+ /**
606
+ * Creates an empty container with no dependencies.
698
607
  *
699
- * @example Overriding a dependency
700
- * ```typescript
701
- * const container = Container.empty()
702
- * .register(DatabaseService, () => new DatabaseService())
703
- * .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
704
- * ```
608
+ * Shorthand for `Container.builder().build()`.
609
+ */
610
+ static empty(): Container;
611
+ /**
612
+ * Creates a scoped container for hierarchical dependency management.
705
613
  *
706
- * @example Using value tags
707
- * ```typescript
708
- * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
614
+ * Scoped containers support parent/child relationships where children
615
+ * can access parent dependencies but maintain their own cache.
709
616
  *
710
- * const container = Container.empty().register(
711
- * ConfigTag,
712
- * () => ({ apiUrl: 'https://api.example.com' })
713
- * );
714
- * ```
617
+ * @param scope - Identifier for the scope (for debugging)
715
618
  *
716
- * @example With finalizer for cleanup
619
+ * @example
717
620
  * ```typescript
718
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
719
- * async connect() { return; }
720
- * async close() { return; }
721
- * }
621
+ * const appContainer = Container.scoped('app');
622
+ * // ... add app-level dependencies
722
623
  *
723
- * const container = Container.empty().register(
724
- * DatabaseConnection,
725
- * async () => {
726
- * const conn = new DatabaseConnection();
727
- * await conn.connect();
728
- * return conn;
729
- * },
730
- * (conn) => conn.close() // Called during container.destroy()
731
- * );
624
+ * const requestContainer = appContainer.child('request');
625
+ * // ... add request-specific dependencies
732
626
  * ```
733
627
  */
734
- register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): Container<TTags | T>;
628
+ static scoped(scope: string | symbol): ScopedContainer;
735
629
  /**
736
- * Checks if a dependency has been registered in the container.
630
+ * Creates a container from a layer.
737
631
  *
738
- * This returns `true` if the dependency has been registered via `.register()`,
739
- * regardless of whether it has been instantiated yet.
632
+ * This is a convenience method equivalent to applying a layer to
633
+ * `Container.builder()` and building the result.
740
634
  *
741
- * @param tag - The dependency tag to check
742
- * @returns `true` if the dependency has been registered, `false` otherwise
635
+ * @param layer - A layer with no requirements (all dependencies satisfied)
743
636
  *
744
637
  * @example
745
638
  * ```typescript
746
- * const container = Container.empty().register(DatabaseService, () => new DatabaseService());
747
- * console.log(c.has(DatabaseService)); // true
748
- * ```
749
- */
750
- has(tag: AnyTag): boolean;
751
- /**
752
- * Checks if a dependency has been instantiated (cached) in the container.
639
+ * const dbLayer = Layer.service(Database, []);
640
+ * const container = Container.from(dbLayer);
753
641
  *
754
- * @param tag - The dependency tag to check
755
- * @returns true if the dependency has been instantiated, false otherwise
642
+ * const db = await container.resolve(Database);
643
+ * ```
756
644
  */
757
- exists(tag: AnyTag): boolean;
645
+ static from<TProvides extends AnyTag>(layer: Layer<never, TProvides>): Container<TProvides>;
758
646
  /**
759
- * Retrieves a dependency instance from the container, creating it if necessary.
647
+ * Resolves a dependency, creating it if necessary.
760
648
  *
761
- * This method ensures singleton behavior - each dependency is created only once
762
- * and cached for subsequent calls. The method is async-safe and handles concurrent
763
- * requests for the same dependency correctly.
649
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
764
650
  *
765
- * The method performs circular dependency detection by tracking the resolution chain
766
- * through the resolution context.
767
- *
768
- * @template T - The dependency tag type (must be registered in this container)
769
- * @param tag - The dependency tag to retrieve
770
- * @returns Promise resolving to the service instance
771
- * @throws {UnknownDependencyError} If the dependency is not registered
651
+ * @param tag - The dependency tag to resolve
652
+ * @returns Promise resolving to the dependency instance
653
+ * @throws {ContainerDestroyedError} If the container has been destroyed
654
+ * @throws {UnknownDependencyError} If any dependency is not registered
772
655
  * @throws {CircularDependencyError} If a circular dependency is detected
773
- * @throws {DependencyCreationError} If the factory function throws an error
774
- *
775
- * @example Basic usage
776
- * ```typescript
777
- * const container = Container.empty()
778
- * .register(DatabaseService, () => new DatabaseService());
779
- *
780
- * const db = await c.resolve(DatabaseService);
781
- * db.query('SELECT * FROM users');
782
- * ```
783
- *
784
- * @example Concurrent access (singleton behavior)
785
- * ```typescript
786
- * // All three calls will receive the same instance
787
- * const [db1, db2, db3] = await Promise.all([
788
- * c.resolve(DatabaseService),
789
- * c.resolve(DatabaseService),
790
- * c.resolve(DatabaseService)
791
- * ]);
792
- *
793
- * console.log(db1 === db2 === db3); // true
794
- * ```
795
- *
796
- * @example Dependency injection in factories
797
- * ```typescript
798
- * const container = Container.empty()
799
- * .register(DatabaseService, () => new DatabaseService())
800
- * .register(UserService, async (ctx) => {
801
- * const db = await ctx.resolve(DatabaseService);
802
- * return new UserService(db);
803
- * });
804
- *
805
- * const userService = await c.resolve(UserService);
806
- * ```
656
+ * @throws {DependencyCreationError} If any factory function throws an error
807
657
  */
808
658
  resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
809
659
  /**
810
- * Internal resolution method that tracks the dependency chain for circular dependency detection.
811
- * Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
660
+ * Internal resolution with dependency chain tracking.
812
661
  * @internal
813
662
  */
814
663
  protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
815
664
  /**
816
- * Resolves multiple dependencies concurrently using Promise.all.
817
- *
818
- * This method takes a variable number of dependency tags and resolves all of them concurrently,
819
- * returning a tuple with the resolved instances in the same order as the input tags.
820
- * The method maintains all the same guarantees as the individual resolve method:
821
- * singleton behavior, circular dependency detection, and proper error handling.
665
+ * Resolves multiple dependencies concurrently.
822
666
  *
823
- * @template T - The tuple type of dependency tags to resolve
824
- * @param tags - Variable number of dependency tags to resolve
825
- * @returns Promise resolving to a tuple of service instances in the same order
667
+ * @param tags - The dependency tags to resolve
668
+ * @returns Promise resolving to a tuple of instances
826
669
  * @throws {ContainerDestroyedError} If the container has been destroyed
827
670
  * @throws {UnknownDependencyError} If any dependency is not registered
828
671
  * @throws {CircularDependencyError} If a circular dependency is detected
829
672
  * @throws {DependencyCreationError} If any factory function throws an error
830
- *
831
- * @example Basic usage
832
- * ```typescript
833
- * const container = Container.empty()
834
- * .register(DatabaseService, () => new DatabaseService())
835
- * .register(LoggerService, () => new LoggerService());
836
- *
837
- * const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
838
- * ```
839
- *
840
- * @example Mixed tag types
841
- * ```typescript
842
- * const ApiKeyTag = Tag.of('apiKey')<string>();
843
- * const container = Container.empty()
844
- * .register(ApiKeyTag, () => 'secret-key')
845
- * .register(UserService, () => new UserService());
846
- *
847
- * const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
848
- * ```
849
- *
850
- * @example Empty array
851
- * ```typescript
852
- * const results = await c.resolveAll(); // Returns empty array
853
- * ```
854
673
  */
855
674
  resolveAll<const T extends readonly TTags[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
856
675
  /**
857
- * Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
858
- *
859
- * **Important: After calling destroy(), the container becomes permanently unusable.**
860
- * Any subsequent calls to register(), get(), or destroy() will throw a DependencyFinalizationError.
861
- * This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
862
- *
863
- * All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
864
- * for maximum cleanup performance.
865
- * If any finalizers fail, all errors are collected and a DependencyFinalizationError
866
- * is thrown containing details of all failures.
676
+ * Resolves a service, runs the callback with it, then destroys the container.
867
677
  *
868
- * **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
869
- * Services should be designed to handle cleanup gracefully regardless of the order in which their
870
- * dependencies are cleaned up.
678
+ * This is a convenience method for the common "create, use, destroy" pattern.
679
+ * The container is always destroyed after the callback completes, even if it throws.
871
680
  *
872
- * @returns Promise that resolves when all cleanup is complete
873
- * @throws {DependencyFinalizationError} If any finalizers fail during cleanup
874
- *
875
- * @example Basic cleanup
876
- * ```typescript
877
- * const container = Container.empty()
878
- * .register(DatabaseConnection,
879
- * async () => {
880
- * const conn = new DatabaseConnection();
881
- * await conn.connect();
882
- * return conn;
883
- * },
884
- * (conn) => conn.disconnect() // Finalizer
885
- * );
886
- *
887
- * const db = await c.resolve(DatabaseConnection);
888
- * await c.destroy(); // Calls conn.disconnect(), container becomes unusable
889
- *
890
- * // This will throw an error
891
- * try {
892
- * await c.resolve(DatabaseConnection);
893
- * } catch (error) {
894
- * console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
895
- * }
896
- * ```
897
- *
898
- * @example Application shutdown
899
- * ```typescript
900
- * const appContainer Container.empty
901
- * .register(DatabaseService, () => new DatabaseService())
902
- * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
903
- *
904
- * // During application shutdown
905
- * process.on('SIGTERM', async () => {
906
- * try {
907
- * await appContainer.destroy(); // Clean shutdown of all services
908
- * } catch (error) {
909
- * console.error('Error during shutdown:', error);
910
- * }
911
- * process.exit(0);
912
- * });
913
- * ```
681
+ * @param tag - The dependency tag to resolve
682
+ * @param fn - Callback that receives the resolved service
683
+ * @returns Promise resolving to the callback's return value
684
+ * @throws {ContainerDestroyedError} If the container has been destroyed
685
+ * @throws {UnknownDependencyError} If the dependency is not registered
686
+ * @throws {CircularDependencyError} If a circular dependency is detected
687
+ * @throws {DependencyCreationError} If the factory function throws
688
+ * @throws {DependencyFinalizationError} If the finalizer function throws
914
689
  *
915
- * @example Handling cleanup errors
690
+ * @example
916
691
  * ```typescript
917
- * try {
918
- * await container.destroy();
919
- * } catch (error) {
920
- * if (error instanceof DependencyContainerFinalizationError) {
921
- * console.error('Some dependencies failed to clean up:', error.detail.errors);
922
- * }
923
- * }
924
- * // Container is destroyed regardless of finalizer errors
692
+ * const result = await container.use(UserService, (service) =>
693
+ * service.getUsers()
694
+ * );
695
+ * // Container is automatically destroyed after callback completes
925
696
  * ```
926
697
  */
927
- destroy(): Promise<void>;
928
- }
929
-
930
- //#endregion
931
- //#region src/scoped-container.d.ts
932
- type Scope = string | symbol;
933
- declare class ScopedContainer<TTags extends AnyTag> extends Container<TTags> {
934
- readonly scope: Scope;
935
- private parent;
936
- private readonly children;
937
- protected constructor(parent: IContainer<TTags> | null, scope: Scope);
938
- /**
939
- * Creates a new empty scoped container instance.
940
- * @param scope - The scope identifier for this container
941
- * @returns A new empty ScopedContainer instance with no registered dependencies
942
- */
943
- static empty(scope: Scope): ScopedContainer<never>;
944
- /**
945
- * Registers a dependency in the scoped container.
946
- *
947
- * Overrides the base implementation to return ScopedContainer type
948
- * for proper method chaining support.
949
- */
950
- register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainer<TTags | T>;
951
- /**
952
- * Checks if a dependency has been registered in this scope or any parent scope.
953
- *
954
- * This method checks the current scope first, then walks up the parent chain.
955
- * Returns true if the dependency has been registered somewhere in the scope hierarchy.
956
- */
957
- has(tag: AnyTag): boolean;
958
- /**
959
- * Checks if a dependency has been instantiated in this scope or any parent scope.
960
- *
961
- * This method checks the current scope first, then walks up the parent chain.
962
- * Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
963
- */
964
- exists(tag: AnyTag): boolean;
965
- /**
966
- * Retrieves a dependency instance, resolving from the current scope or parent scopes.
967
- *
968
- * Resolution strategy:
969
- * 1. Check cache in current scope
970
- * 2. Check if factory exists in current scope - if so, create instance here
971
- * 3. Otherwise, delegate to parent scope
972
- * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
973
- */
974
- resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
975
- /**
976
- * Internal resolution with delegation logic for scoped containers.
977
- * @internal
978
- */
979
- protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
698
+ use<T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>): Promise<R>;
980
699
  /**
981
- * Destroys this scoped container and its children, preserving the container structure for reuse.
700
+ * Destroys the container, calling all finalizers.
982
701
  *
983
- * This method ensures proper cleanup order while maintaining reusability:
984
- * 1. Destroys all child scopes first (they may depend on parent scope dependencies)
985
- * 2. Then calls finalizers for dependencies created in this scope
986
- * 3. Clears only instance caches - preserves factories, finalizers, and child structure
702
+ * After destruction, the container cannot be used.
703
+ * Finalizers run concurrently, so there are no ordering guarantees.
704
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
705
+ * dependencies are cleaned up.
987
706
  *
988
- * Child destruction happens first to ensure dependencies don't get cleaned up
989
- * before their dependents.
707
+ * @throws {DependencyFinalizationError} If any finalizers fail
990
708
  */
991
709
  destroy(): Promise<void>;
992
- /**
993
- * Creates a child scoped container.
994
- *
995
- * Child containers inherit access to parent dependencies but maintain
996
- * their own scope for new registrations and instance caching.
997
- */
998
- child(scope: Scope): ScopedContainer<TTags>;
999
710
  }
1000
-
1001
- //#endregion
1002
- //#region src/layer.d.ts
1003
711
  /**
1004
- * Replaces the TTags type parameter in a container type with a new type.
1005
- * Preserves the concrete container type (Container, ScopedContainer, or IContainer).
1006
- *
1007
- * Uses contravariance to detect container types:
1008
- * - Any ScopedContainer<X> extends ScopedContainer<never>
1009
- * - Any Container<X> extends Container<never> (but not ScopedContainer<never>)
1010
- * - Falls back to IContainer for anything else
1011
- * @internal
1012
- */
1013
- type WithContainerTags<TContainer, TNewTags extends AnyTag> = TContainer extends ScopedContainer<never> ? ScopedContainer<TNewTags> : TContainer extends Container<never> ? Container<TNewTags> : IContainer<TNewTags>;
1014
- /**
1015
- * The most generic layer type that accepts any concrete layer.
712
+ * Scope identifier type.
1016
713
  */
1017
- type AnyLayer = Layer<any, any>;
1018
- /**
1019
- * The type ID for the Layer interface.
1020
- */
1021
- declare const LayerTypeId: unique symbol;
714
+ type Scope = string | symbol;
1022
715
  /**
1023
- * A dependency layer represents a reusable, composable unit of dependency registrations.
1024
- * Layers allow you to organize your dependency injection setup into logical groups
1025
- * that can be combined and reused across different contexts.
1026
- *
1027
- * ## Type Variance
1028
- *
1029
- * The Layer interface uses TypeScript's variance annotations to enable safe substitutability:
1030
- *
1031
- * ### TRequires (covariant)
1032
- * A layer requiring fewer dependencies can substitute one requiring more:
1033
- * - `Layer<never, X>` can be used where `Layer<A | B, X>` is expected
1034
- * - Intuition: A service that needs nothing is more flexible than one that needs specific deps
1035
- *
1036
- * ### TProvides (contravariant)
1037
- * A layer providing more services can substitute one providing fewer:
1038
- * - `Layer<X, A | B>` can be used where `Layer<X, A>` is expected
1039
- * - Intuition: A service that gives you extra things is compatible with expecting fewer things
1040
- *
1041
- * @template TRequires - The union of tags this layer requires to be satisfied by other layers
1042
- * @template TProvides - The union of tags this layer provides/registers
1043
- *
1044
- * @example Basic layer usage
1045
- * ```typescript
1046
- * import { layer, Tag, container } from 'sandly';
1047
- *
1048
- * class DatabaseService extends Tag.Service('DatabaseService') {
1049
- * query() { return 'data'; }
1050
- * }
1051
- *
1052
- * // Create a layer that provides DatabaseService
1053
- * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
1054
- * container.register(DatabaseService, () => new DatabaseService())
1055
- * );
1056
- *
1057
- * // Apply the layer to a container
1058
- * const container = Container.empty();
1059
- * const finalContainer = databaseLayer.register(c);
1060
- *
1061
- * const db = await finalContainer.resolve(DatabaseService);
1062
- * ```
1063
- *
1064
- * @example Layer composition with variance
1065
- * ```typescript
1066
- * // Layer that requires DatabaseService and provides UserService
1067
- * const userLayer = layer<typeof DatabaseService, typeof UserService>((container) =>
1068
- * container.register(UserService, async (ctx) =>
1069
- * new UserService(await ctx.resolve(DatabaseService))
1070
- * )
1071
- * );
716
+ * Builder for constructing scoped containers.
1072
717
  *
1073
- * // Compose layers: provide database layer to user layer
1074
- * const appLayer = userLayer.provide(databaseLayer);
1075
- * ```
718
+ * @template TTags - Union type of registered dependency tags
1076
719
  */
1077
- interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
1078
- readonly [LayerTypeId]?: {
1079
- readonly _TRequires: Covariant<TRequires>;
1080
- readonly _TProvides: Contravariant<TProvides>;
1081
- };
720
+ declare class ScopedContainerBuilder<TTags extends AnyTag = never> {
721
+ private readonly scope;
722
+ private readonly parent;
723
+ private readonly factories;
724
+ private readonly finalizers;
725
+ constructor(scope: Scope, parent: IContainer<TTags> | null);
1082
726
  /**
1083
- * Applies this layer's registrations to the given container.
1084
- *
1085
- * ## Generic Container Support
1086
- *
1087
- * The signature uses `TContainer extends AnyTag` to accept containers with any existing
1088
- * services while preserving type information. The container must provide at least this
1089
- * layer's requirements (`TRequires`) but can have additional services (`TContainer`).
1090
- *
1091
- * Result container has: `TRequires | TContainer | TProvides` - everything that was
1092
- * already there plus this layer's new provisions.
1093
- *
1094
- * @param container - The container to register dependencies into (must satisfy TRequires)
1095
- * @returns A new container with this layer's dependencies registered and all existing services preserved
1096
- *
1097
- * @example Basic usage
1098
- * ```typescript
1099
- * const container = Container.empty();
1100
- * const updatedContainer = myLayer.register(c);
1101
- * ```
1102
- *
1103
- * @example With existing services preserved
1104
- * ```typescript
1105
- * const baseContainer = Container.empty()
1106
- * .register(ExistingService, () => new ExistingService());
1107
- *
1108
- * const enhanced = myLayer.register(baseContainer);
1109
- * // Enhanced container has both ExistingService and myLayer's provisions
1110
- * ```
727
+ * Registers a dependency with a factory function or lifecycle object.
1111
728
  */
1112
- register: <TContainer extends IContainer<TRequires>>(container: TContainer) => WithContainerTags<TContainer, ContainerTags<TContainer> | TProvides>;
1113
- /**
1114
- * Provides a dependency layer to this layer, creating a pipeline where the dependency layer's
1115
- * provisions satisfy this layer's requirements. This creates a dependency flow from dependency → this.
1116
- *
1117
- * Type-safe: This layer's requirements must be satisfiable by the dependency layer's
1118
- * provisions and any remaining external requirements.
1119
- *
1120
- * @template TDepRequires - What the dependency layer requires
1121
- * @template TDepProvides - What the dependency layer provides
1122
- * @param dependency - The layer to provide as a dependency
1123
- * @returns A new composed layer that only exposes this layer's provisions
1124
- *
1125
- * @example Simple composition
1126
- * ```typescript
1127
- * const configLayer = layer<never, typeof ConfigTag>(...);
1128
- * const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
1129
- *
1130
- * // Provide config to database layer
1131
- * const infraLayer = dbLayer.provide(configLayer);
1132
- * ```
1133
- *
1134
- * @example Multi-level composition (reads naturally left-to-right)
1135
- * ```typescript
1136
- * const appLayer = apiLayer
1137
- * .provide(serviceLayer)
1138
- * .provide(databaseLayer)
1139
- * .provide(configLayer);
1140
- * ```
1141
- */
1142
- provide: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides>;
1143
- /**
1144
- * Provides a dependency layer to this layer and merges the provisions.
1145
- * Unlike `.provide()`, this method includes both this layer's provisions and the dependency layer's
1146
- * provisions in the result type. This is useful when you want to expose services from both layers.
1147
- *
1148
- * Type-safe: This layer's requirements must be satisfiable by the dependency layer's
1149
- * provisions and any remaining external requirements.
1150
- *
1151
- * @template TDepRequires - What the dependency layer requires
1152
- * @template TDepProvides - What the dependency layer provides
1153
- * @param dependency - The layer to provide as a dependency
1154
- * @returns A new composed layer that provides services from both layers
1155
- *
1156
- * @example Providing with merged provisions
1157
- * ```typescript
1158
- * const configLayer = layer<never, typeof ConfigTag>(...);
1159
- * const dbLayer = layer<typeof ConfigTag, typeof DatabaseService>(...);
1160
- *
1161
- * // Provide config to database layer, and both services are available
1162
- * const infraLayer = dbLayer.provideMerge(configLayer);
1163
- * // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
1164
- * ```
1165
- *
1166
- * @example Difference from .provide()
1167
- * ```typescript
1168
- * // .provide() only exposes this layer's provisions:
1169
- * const withProvide = dbLayer.provide(configLayer);
1170
- * // Type: Layer<never, typeof DatabaseService>
1171
- *
1172
- * // .provideMerge() exposes both layers' provisions:
1173
- * const withProvideMerge = dbLayer.provideMerge(configLayer);
1174
- * // Type: Layer<never, typeof ConfigTag | typeof DatabaseService>
1175
- * ```
1176
- */
1177
- provideMerge: <TDepRequires extends AnyTag, TDepProvides extends AnyTag>(dependency: Layer<TDepRequires, TDepProvides>) => Layer<TDepRequires | Exclude<TRequires, TDepProvides>, TProvides | TDepProvides>;
1178
- /**
1179
- * Merges this layer with another layer, combining their requirements and provisions.
1180
- * This is useful for combining independent layers that don't have a dependency
1181
- * relationship.
1182
- *
1183
- * @template TOtherRequires - What the other layer requires
1184
- * @template TOtherProvides - What the other layer provides
1185
- * @param other - The layer to merge with
1186
- * @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
1187
- *
1188
- * @example Merging independent layers
1189
- * ```typescript
1190
- * const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
1191
- * const loggingLayer = layer<never, typeof LoggerService>(...);
1192
- *
1193
- * // Combine infrastructure layers
1194
- * const infraLayer = persistenceLayer.merge(loggingLayer);
1195
- * ```
1196
- *
1197
- * @example Building complex layer combinations
1198
- * ```typescript
1199
- * const appInfraLayer = persistenceLayer
1200
- * .merge(messagingLayer)
1201
- * .merge(observabilityLayer);
1202
- * ```
729
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainerBuilder<TTags | T>;
730
+ /**
731
+ * Creates an immutable scoped container from the registered dependencies.
1203
732
  */
1204
- merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
733
+ build(): ScopedContainer<TTags>;
1205
734
  }
1206
735
  /**
1207
- * Creates a new dependency layer that encapsulates a set of dependency registrations.
1208
- * Layers are the primary building blocks for organizing and composing dependency injection setups.
1209
- *
1210
- * @template TRequires - The union of dependency tags this layer requires from other layers or external setup
1211
- * @template TProvides - The union of dependency tags this layer registers/provides
1212
- *
1213
- * @param register - Function that performs the dependency registrations. Receives a container.
1214
- * @returns The layer instance.
1215
- *
1216
- * @example Simple layer
1217
- * ```typescript
1218
- * import { layer, Tag } from 'sandly';
1219
- *
1220
- * class DatabaseService extends Tag.Service('DatabaseService') {
1221
- * constructor(private url: string = 'sqlite://memory') {}
1222
- * query() { return 'data'; }
1223
- * }
736
+ * Scoped container for hierarchical dependency management.
1224
737
  *
1225
- * // Layer that provides DatabaseService, requires nothing
1226
- * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
1227
- * container.register(DatabaseService, () => new DatabaseService())
1228
- * );
738
+ * Supports parent/child relationships where children can access parent
739
+ * dependencies but maintain their own cache. Useful for request-scoped
740
+ * dependencies in web applications.
1229
741
  *
1230
- * // Usage
1231
- * const dbLayerInstance = databaseLayer;
1232
- * ```
742
+ * @template TTags - Union type of registered dependency tags
1233
743
  *
1234
- * @example Complex application layer structure
744
+ * @example
1235
745
  * ```typescript
1236
- * // Configuration layer
1237
- * const configLayer = layer<never, typeof ConfigTag>((container) =>
1238
- * container.register(ConfigTag, () => loadConfig())
1239
- * );
1240
- *
1241
- * // Infrastructure layer (requires config)
1242
- * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
1243
- * (container) =>
1244
- * container
1245
- * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
1246
- * .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
1247
- * );
1248
- *
1249
- * // Service layer (requires infrastructure)
1250
- * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
1251
- * (container) =>
1252
- * container.register(UserService, async (ctx) =>
1253
- * new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
1254
- * )
1255
- * );
1256
- *
1257
- * // Compose the complete application
1258
- * const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
746
+ * // Application-level container
747
+ * const appContainer = ScopedContainer.builder('app')
748
+ * .add(Database, () => new Database())
749
+ * .build();
750
+ *
751
+ * // Request-level container (inherits from app)
752
+ * const requestContainer = appContainer.child('request')
753
+ * .add(RequestContext, () => new RequestContext())
754
+ * .build();
755
+ *
756
+ * // Can resolve both app and request dependencies
757
+ * const db = await requestContainer.resolve(Database);
758
+ * const ctx = await requestContainer.resolve(RequestContext);
1259
759
  * ```
1260
760
  */
1261
- 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>;
1262
- /**
1263
- * Helper type that extracts the union of all requirements from an array of layers.
1264
- * Used by Layer.mergeAll() to compute the correct requirement type for the merged layer.
1265
- *
1266
- * Works with AnyLayer[] constraint which accepts any concrete layer through variance:
1267
- * - Layer<never, X> → extracts `never` (no requirements)
1268
- * - Layer<A | B, Y> extracts `A | B` (specific requirements)
1269
- *
1270
- * @internal
1271
- */
1272
- type UnionOfRequires<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<infer R, any> ? R : never }[number];
1273
- /**
1274
- * Helper type that extracts the union of all provisions from an array of layers.
1275
- * Used by Layer.mergeAll() to compute the correct provision type for the merged layer.
1276
- *
1277
- * Works with AnyLayer[] constraint which accepts any concrete layer through variance:
1278
- * - Layer<X, never> → extracts `never` (no provisions)
1279
- * - Layer<Y, A | B> → extracts `A | B` (specific provisions)
1280
- *
1281
- * @internal
1282
- */
1283
- type UnionOfProvides<T extends readonly AnyLayer[]> = { [K in keyof T]: T[K] extends Layer<any, infer P> ? P : never }[number];
1284
- /**
1285
- * Utility object containing helper functions for working with layers.
1286
- */
1287
- declare const Layer: {
761
+ declare class ScopedContainer<TTags extends AnyTag = never> extends Container<TTags> {
762
+ readonly scope: Scope;
763
+ private parent;
764
+ private readonly children;
765
+ /**
766
+ * @internal
767
+ */
768
+ protected constructor(scope: Scope, parent: IContainer<TTags> | null, factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
1288
769
  /**
1289
- * Creates an empty layer that provides no dependencies and requires no dependencies.
1290
- * This is useful as a base layer or for testing.
770
+ * @internal - Used by ScopedContainerBuilder
771
+ */
772
+ static _createScopedFromBuilder<T extends AnyTag>(scope: Scope, parent: IContainer<T> | null, factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): ScopedContainer<T>;
773
+ /**
774
+ * Creates a new scoped container builder.
1291
775
  *
1292
- * @returns An empty layer that can be used as a starting point for layer composition
776
+ * @param scope - Identifier for the scope (for debugging)
1293
777
  *
1294
778
  * @example
1295
779
  * ```typescript
1296
- * import { Layer } from 'sandly';
1297
- *
1298
- * const baseLayer = Layer.empty();
1299
- * const appLayer = baseLayer
1300
- * .merge(configLayer)
1301
- * .merge(serviceLayer);
780
+ * const container = ScopedContainer.builder('app')
781
+ * .add(Database, () => new Database())
782
+ * .build();
1302
783
  * ```
1303
784
  */
1304
- empty(): Layer<never, never>;
785
+ static builder(scope: Scope): ScopedContainerBuilder;
1305
786
  /**
1306
- * Merges multiple layers at once in a type-safe way.
1307
- * This is equivalent to chaining `.merge()` calls but more convenient for multiple layers.
1308
- *
1309
- * ## Type Safety with Variance
787
+ * Creates an empty scoped container with no dependencies.
788
+ */
789
+ static empty(scope: Scope): ScopedContainer;
790
+ /**
791
+ * Creates a scoped container from a layer.
1310
792
  *
1311
- * Uses the AnyLayer constraint (Layer<never, AnyTag>) which accepts any concrete layer
1312
- * through the Layer interface's variance annotations:
793
+ * This is a convenience method equivalent to applying a layer to
794
+ * `ScopedContainer.builder()` and building the result.
1313
795
  *
1314
- * - **Contravariant TRequires**: Layer<typeof ServiceA, X> can be passed because requiring
1315
- * ServiceA is more restrictive than requiring `never` (nothing)
1316
- * - **Covariant TProvides**: Layer<Y, typeof ServiceB> can be passed because providing
1317
- * ServiceB is compatible with the general `AnyTag` type
796
+ * @param scope - Identifier for the scope (for debugging)
797
+ * @param layer - A layer with no requirements (all dependencies satisfied)
1318
798
  *
1319
- * The return type correctly extracts and unions the actual requirement/provision types
1320
- * from all input layers, preserving full type safety.
799
+ * @example
800
+ * ```typescript
801
+ * const dbLayer = Layer.service(Database, []);
802
+ * const container = ScopedContainer.from('app', dbLayer);
1321
803
  *
1322
- * All layers are merged in order, combining their requirements and provisions.
1323
- * The resulting layer requires the union of all input layer requirements and
1324
- * provides the union of all input layer provisions.
804
+ * const db = await container.resolve(Database);
805
+ * ```
806
+ */
807
+ static from<TProvides extends AnyTag>(scope: Scope, layer: Layer<never, TProvides>): ScopedContainer<TProvides>;
808
+ /**
809
+ * Resolves a dependency from this scope or parent scopes, creating it if necessary.
1325
810
  *
1326
- * @template T - The tuple type of layers to merge (constrained to AnyLayer for variance)
1327
- * @param layers - At least 2 layers to merge together
1328
- * @returns A new layer that combines all input layers with correct union types
811
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
1329
812
  *
1330
- * @example Basic usage with different layer types
1331
- * ```typescript
1332
- * import { Layer } from 'sandly';
813
+ * @param tag - The dependency tag to resolve
814
+ * @returns Promise resolving to the dependency instance
815
+ * @throws {ContainerDestroyedError} If the container has been destroyed
816
+ * @throws {UnknownDependencyError} If any dependency is not registered
817
+ * @throws {CircularDependencyError} If a circular dependency is detected
818
+ * @throws {DependencyCreationError} If any factory function throws an error
819
+ */
820
+ resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
821
+ /**
822
+ * Internal resolution with parent delegation.
823
+ * @internal
824
+ */
825
+ protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
826
+ /**
827
+ * @internal - Used by ScopedContainerBuilder to register children
828
+ */
829
+ _registerChild(child: ScopedContainer<TTags>): void;
830
+ /**
831
+ * Creates a child container builder that inherits from this container.
1333
832
  *
1334
- * // These all have different types but work thanks to variance:
1335
- * const dbLayer = layer<never, typeof DatabaseService>(...); // no requirements
1336
- * const userLayer = layer<typeof DatabaseService, typeof UserService>(...); // requires DB
1337
- * const configLayer = layer<never, typeof ConfigService>(...); // no requirements
833
+ * Use this to create a child scope and add dependencies to it.
834
+ * The child can resolve dependencies from this container.
1338
835
  *
1339
- * const infraLayer = Layer.mergeAll(dbLayer, userLayer, configLayer);
1340
- * // Type: Layer<typeof DatabaseService, typeof DatabaseService | typeof UserService | typeof ConfigService>
1341
- * ```
836
+ * @param scope - Identifier for the child scope
837
+ * @returns A new ScopedContainerBuilder for the child scope
838
+ * @throws {ContainerDestroyedError} If the container has been destroyed
1342
839
  *
1343
- * @example Equivalent to chaining .merge()
840
+ * @example
1344
841
  * ```typescript
1345
- * // These are equivalent:
1346
- * const layer1 = Layer.mergeAll(layerA, layerB, layerC);
1347
- * const layer2 = layerA.merge(layerB).merge(layerC);
842
+ * const requestContainer = appContainer.child('request')
843
+ * .add(RequestContext, () => new RequestContext())
844
+ * .build();
845
+ *
846
+ * await requestContainer.resolve(Database); // From parent
847
+ * await requestContainer.resolve(RequestContext); // From this scope
1348
848
  * ```
849
+ */
850
+ child(scope: Scope): ScopedContainerBuilder<TTags>;
851
+ /**
852
+ * Creates a child container with a layer applied.
1349
853
  *
1350
- * @example Building infrastructure layers
1351
- * ```typescript
1352
- * const persistenceLayer = layer<never, typeof DatabaseService | typeof CacheService>(...);
1353
- * const messagingLayer = layer<never, typeof MessageQueue>(...);
1354
- * const observabilityLayer = layer<never, typeof Logger | typeof Metrics>(...);
854
+ * This is a convenience method combining child() + layer.apply() + build().
855
+ * Use this when you have a layer ready to apply.
1355
856
  *
1356
- * // Merge all infrastructure concerns into one layer
1357
- * const infraLayer = Layer.mergeAll(
1358
- * persistenceLayer,
1359
- * messagingLayer,
1360
- * observabilityLayer
857
+ * @param scope - Identifier for the child scope
858
+ * @param layer - Layer to apply to the child (can require parent's tags)
859
+ *
860
+ * @example
861
+ * ```typescript
862
+ * const requestContainer = appContainer.childFrom('request',
863
+ * userService
864
+ * .provide(Layer.value(TenantContext, tenantCtx))
865
+ * .provide(Layer.value(RequestId, requestId))
1361
866
  * );
1362
867
  *
1363
- * // Result type: Layer<never, DatabaseService | CacheService | MessageQueue | Logger | Metrics>
868
+ * const users = await requestContainer.resolve(UserService);
1364
869
  * ```
1365
870
  */
1366
- mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
871
+ childFrom<TProvides extends AnyTag>(scope: Scope, layer: Layer<TTags, TProvides>): ScopedContainer<TTags | TProvides>;
1367
872
  /**
1368
- * Merges exactly two layers, combining their requirements and provisions.
1369
- * This is similar to the `.merge()` method but available as a static function.
1370
- *
1371
- * @template TRequires1 - What the first layer requires
1372
- * @template TProvides1 - What the first layer provides
1373
- * @template TRequires2 - What the second layer requires
1374
- * @template TProvides2 - What the second layer provides
1375
- * @param layer1 - The first layer to merge
1376
- * @param layer2 - The second layer to merge
1377
- * @returns A new merged layer requiring both layers' requirements and providing both layers' provisions
1378
- *
1379
- * @example Merging two layers
1380
- * ```typescript
1381
- * import { Layer } from 'sandly';
873
+ * Destroys this container and all child containers.
1382
874
  *
1383
- * const dbLayer = layer<never, typeof DatabaseService>(...);
1384
- * const cacheLayer = layer<never, typeof CacheService>(...);
875
+ * Children are destroyed first to ensure proper cleanup order.
1385
876
  *
1386
- * const persistenceLayer = Layer.merge(dbLayer, cacheLayer);
1387
- * // Type: Layer<never, typeof DatabaseService | typeof CacheService>
1388
- * ```
877
+ * After destruction, the container cannot be used.
878
+ * Finalizers run concurrently, so there are no ordering guarantees.
879
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
880
+ * dependencies are cleaned up.
881
+ *
882
+ * @throws {DependencyFinalizationError} If any finalizers fail
1389
883
  */
1390
- 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>;
1391
- };
1392
-
1393
- //#endregion
1394
- //#region src/constant.d.ts
1395
- /**
1396
- * Creates a layer that provides a constant value for a given tag.
1397
- *
1398
- * @param tag - The value tag to provide
1399
- * @param constantValue - The constant value to provide
1400
- * @returns A layer with no dependencies that provides the constant value
1401
- *
1402
- * @example
1403
- * ```typescript
1404
- * const ApiKey = Tag.of('ApiKey')<string>();
1405
- * const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
1406
- *
1407
- * const apiKey = constant(ApiKey, 'my-secret-key');
1408
- * const dbUrl = constant(DatabaseUrl, 'postgresql://localhost:5432/myapp');
1409
- *
1410
- * const config = Layer.merge(apiKey, dbUrl);
1411
- * ```
1412
- */
1413
- declare function constant<T extends ValueTag<TagId, unknown>>(tag: T, constantValue: TagType<T>): Layer<never, T>;
884
+ destroy(): Promise<void>;
885
+ }
1414
886
 
1415
887
  //#endregion
1416
- //#region src/dependency.d.ts
1417
- /**
1418
- * Extracts a union type from a tuple of tags.
1419
- * Returns `never` for empty arrays.
1420
- * @internal
1421
- */
1422
- type TagsToUnion<T extends readonly AnyTag[]> = T[number];
888
+ //#region src/errors.d.ts
1423
889
  /**
1424
- * Creates a layer that provides a single dependency with inferred requirements.
1425
- *
1426
- * This is a simplified alternative to `layer()` for the common case of defining
1427
- * a single dependency. Unlike `service()` and `autoService()`, this works with
1428
- * any tag type (ServiceTag or ValueTag) and doesn't require extending `Tag.Service()`.
1429
- *
1430
- * Requirements are passed as an optional array of tags, allowing TypeScript to infer
1431
- * both the tag type and the requirements automatically - no explicit type
1432
- * parameters needed.
1433
- *
1434
- * @param tag - The tag (ServiceTag or ValueTag) that identifies this dependency
1435
- * @param spec - Factory function or lifecycle object for creating the dependency
1436
- * @param requirements - Optional array of dependency tags this dependency requires (defaults to [])
1437
- * @returns A layer that requires the specified dependencies and provides the tag
1438
- *
1439
- * @example Simple dependency without requirements
1440
- * ```typescript
1441
- * const Config = Tag.of('Config')<{ apiUrl: string }>();
1442
- *
1443
- * // No requirements - can omit the array
1444
- * const configDep = dependency(Config, () => ({
1445
- * apiUrl: process.env.API_URL!
1446
- * }));
1447
- * ```
1448
- *
1449
- * @example Dependency with requirements
1450
- * ```typescript
1451
- * const database = dependency(
1452
- * Database,
1453
- * async (ctx) => {
1454
- * const config = await ctx.resolve(Config);
1455
- * const logger = await ctx.resolve(Logger);
1456
- * logger.info('Creating database connection');
1457
- * return createDb(config.DATABASE);
1458
- * },
1459
- * [Config, Logger]
1460
- * );
1461
- * ```
1462
- *
1463
- * @example Dependency with lifecycle (create + cleanup)
1464
- * ```typescript
1465
- * const database = dependency(
1466
- * Database,
1467
- * {
1468
- * create: async (ctx) => {
1469
- * const config = await ctx.resolve(Config);
1470
- * const logger = await ctx.resolve(Logger);
1471
- * logger.info('Creating database connection');
1472
- * return await createDb(config.DATABASE);
1473
- * },
1474
- * cleanup: async (db) => {
1475
- * await disconnectDb(db);
1476
- * },
1477
- * },
1478
- * [Config, Logger]
1479
- * );
1480
- * ```
1481
- *
1482
- * @example Comparison with layer()
1483
- * ```typescript
1484
- * // Using layer() - verbose, requires explicit type parameters
1485
- * const database = layer<typeof Config | typeof Logger, typeof Database>(
1486
- * (container) =>
1487
- * container.register(Database, async (ctx) => {
1488
- * const config = await ctx.resolve(Config);
1489
- * return createDb(config.DATABASE);
1490
- * })
1491
- * );
1492
- *
1493
- * // Using dependency() - cleaner, fully inferred types
1494
- * const database = dependency(
1495
- * Database,
1496
- * async (ctx) => {
1497
- * const config = await ctx.resolve(Config);
1498
- * return createDb(config.DATABASE);
1499
- * },
1500
- * [Config, Logger]
1501
- * );
1502
- * ```
890
+ * Structured error information for debugging and logging.
1503
891
  */
1504
- declare function dependency<TTag extends AnyTag, TRequirements extends readonly AnyTag[] = []>(tag: TTag, spec: DependencySpec<TTag, TagsToUnion<TRequirements>>, requirements?: TRequirements): Layer<TagsToUnion<TRequirements>, TTag>;
1505
-
1506
- //#endregion
1507
- //#region src/errors.d.ts
1508
892
  type ErrorDump = {
1509
893
  name: string;
1510
894
  message: string;
@@ -1512,17 +896,20 @@ type ErrorDump = {
1512
896
  detail: Record<string, unknown>;
1513
897
  cause?: unknown;
1514
898
  };
899
+ /**
900
+ * Options for creating Sandly errors.
901
+ */
1515
902
  type SandlyErrorOptions = {
1516
903
  cause?: unknown;
1517
904
  detail?: Record<string, unknown>;
1518
905
  };
1519
906
  /**
1520
- * Base error class for all library errors.
907
+ * Base error class for all Sandly library errors.
1521
908
  *
1522
- * This extends the native Error class to provide consistent error handling
909
+ * Extends the native Error class to provide consistent error handling
1523
910
  * and structured error information across the library.
1524
911
  *
1525
- * @example Catching library errors
912
+ * @example
1526
913
  * ```typescript
1527
914
  * try {
1528
915
  * await container.resolve(SomeService);
@@ -1540,44 +927,36 @@ declare class SandlyError extends Error {
1540
927
  cause,
1541
928
  detail
1542
929
  }?: SandlyErrorOptions);
930
+ /**
931
+ * Wraps any error as a SandlyError.
932
+ */
1543
933
  static ensure(error: unknown): SandlyError;
934
+ /**
935
+ * Returns a structured representation of the error for logging.
936
+ */
1544
937
  dump(): ErrorDump;
938
+ /**
939
+ * Returns a JSON string representation of the error.
940
+ */
1545
941
  dumps(): string;
1546
942
  /**
1547
943
  * Recursively extract cause chain from any Error.
1548
- * Handles both AppError (with dump()) and plain Errors (with cause property).
1549
944
  */
1550
945
  private dumpCause;
1551
946
  }
1552
- /**
1553
- * Error thrown when attempting to register a dependency that has already been instantiated.
1554
- *
1555
- * This error occurs when calling `container.register()` for a tag that has already been instantiated.
1556
- * Registration must happen before any instantiation occurs, as cached instances would still be used
1557
- * by existing dependencies.
1558
- */
1559
- declare class DependencyAlreadyInstantiatedError extends SandlyError {}
1560
947
  /**
1561
948
  * Error thrown when attempting to use a container that has been destroyed.
1562
- *
1563
- * This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
1564
- * on a container that has already been destroyed. It indicates a programming error where the container
1565
- * is being used after it has been destroyed.
1566
949
  */
1567
950
  declare class ContainerDestroyedError extends SandlyError {}
1568
951
  /**
1569
952
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
1570
953
  *
1571
- * This error occurs when calling `container.resolve(Tag)` for a tag that was never
1572
- * registered via `container.register()`. It indicates a programming error where
1573
- * the dependency setup is incomplete.
1574
- *
1575
954
  * @example
1576
955
  * ```typescript
1577
- * const container = Container.empty(); // Empty container
956
+ * const container = Container.builder().build(); // Empty container
1578
957
  *
1579
958
  * try {
1580
- * await c.resolve(UnregisteredService); // This will throw
959
+ * await container.resolve(UnregisteredService);
1581
960
  * } catch (error) {
1582
961
  * if (error instanceof UnknownDependencyError) {
1583
962
  * console.error('Missing dependency:', error.message);
@@ -1586,360 +965,81 @@ declare class ContainerDestroyedError extends SandlyError {}
1586
965
  * ```
1587
966
  */
1588
967
  declare class UnknownDependencyError extends SandlyError {
1589
- /**
1590
- * @internal
1591
- * Creates an UnknownDependencyError for the given tag.
1592
- *
1593
- * @param tag - The dependency tag that wasn't found
1594
- */
1595
968
  constructor(tag: AnyTag);
1596
969
  }
1597
970
  /**
1598
- * Error thrown when a circular dependency is detected during dependency resolution.
1599
- *
1600
- * This occurs when service A depends on service B, which depends on service A (directly
1601
- * or through a chain of dependencies). The error includes the full dependency chain
1602
- * to help identify the circular reference.
971
+ * Error thrown when a circular dependency is detected during resolution.
1603
972
  *
1604
- * @example Circular dependency scenario
973
+ * @example
1605
974
  * ```typescript
1606
- * class ServiceA extends Tag.Service('ServiceA') {}
1607
- * class ServiceB extends Tag.Service('ServiceB') {}
1608
- *
1609
- * const container = Container.empty()
1610
- * .register(ServiceA, async (ctx) =>
1611
- * new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
1612
- * )
1613
- * .register(ServiceB, async (ctx) =>
1614
- * new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
1615
- * );
1616
- *
975
+ * // ServiceA depends on ServiceB, ServiceB depends on ServiceA
1617
976
  * try {
1618
- * await c.resolve(ServiceA);
977
+ * await container.resolve(ServiceA);
1619
978
  * } catch (error) {
1620
979
  * if (error instanceof CircularDependencyError) {
1621
980
  * console.error('Circular dependency:', error.message);
1622
- * // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
981
+ * // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
1623
982
  * }
1624
983
  * }
1625
984
  * ```
1626
985
  */
1627
986
  declare class CircularDependencyError extends SandlyError {
1628
- /**
1629
- * @internal
1630
- * Creates a CircularDependencyError with the dependency chain information.
1631
- *
1632
- * @param tag - The tag where the circular dependency was detected
1633
- * @param dependencyChain - The chain of dependencies that led to the circular reference
1634
- */
1635
987
  constructor(tag: AnyTag, dependencyChain: AnyTag[]);
1636
988
  }
1637
989
  /**
1638
- * Error thrown when a dependency factory function throws an error during instantiation.
1639
- *
1640
- * This wraps the original error with additional context about which dependency
1641
- * failed to be created. The original error is preserved as the `cause` property.
990
+ * Error thrown when a dependency factory throws during instantiation.
1642
991
  *
1643
- * When dependencies are nested (A depends on B depends on C), and C's factory throws,
1644
- * you get nested DependencyCreationErrors. Use `getRootCause()` to get the original error.
992
+ * For nested dependencies (A depends on B depends on C), use `getRootCause()`
993
+ * to unwrap all layers and get the original error.
1645
994
  *
1646
- * @example Factory throwing error
995
+ * @example
1647
996
  * ```typescript
1648
- * class DatabaseService extends Tag.Service('DatabaseService') {}
1649
- *
1650
- * const container = Container.empty().register(DatabaseService, () => {
1651
- * throw new Error('Database connection failed');
1652
- * });
1653
- *
1654
997
  * try {
1655
- * await c.resolve(DatabaseService);
998
+ * await container.resolve(UserService);
1656
999
  * } catch (error) {
1657
1000
  * if (error instanceof DependencyCreationError) {
1658
1001
  * console.error('Failed to create:', error.message);
1659
- * console.error('Original error:', error.cause);
1660
- * }
1661
- * }
1662
- * ```
1663
- *
1664
- * @example Getting root cause from nested errors
1665
- * ```typescript
1666
- * // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
1667
- * try {
1668
- * await container.resolve(ServiceA);
1669
- * } catch (error) {
1670
- * if (error instanceof DependencyCreationError) {
1671
- * console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
1672
1002
  * const rootCause = error.getRootCause();
1673
- * console.error('Root cause:', rootCause); // Original error from ServiceC
1003
+ * console.error('Root cause:', rootCause);
1674
1004
  * }
1675
1005
  * }
1676
1006
  * ```
1677
1007
  */
1678
1008
  declare class DependencyCreationError extends SandlyError {
1679
- /**
1680
- * @internal
1681
- * Creates a DependencyCreationError wrapping the original factory error.
1682
- *
1683
- * @param tag - The tag of the dependency that failed to be created
1684
- * @param error - The original error thrown by the factory function
1685
- */
1686
1009
  constructor(tag: AnyTag, error: unknown);
1687
1010
  /**
1688
1011
  * Traverses the error chain to find the root cause error.
1689
1012
  *
1690
- * When dependencies are nested, each level wraps the error in a DependencyCreationError.
1691
- * This method unwraps all the layers to get to the original error that started the failure.
1692
- *
1693
- * @returns The root cause error (not a DependencyCreationError unless that's the only error)
1694
- *
1695
- * @example
1696
- * ```typescript
1697
- * try {
1698
- * await container.resolve(UserService);
1699
- * } catch (error) {
1700
- * if (error instanceof DependencyCreationError) {
1701
- * const rootCause = error.getRootCause();
1702
- * console.error('Root cause:', rootCause);
1703
- * }
1704
- * }
1705
- * ```
1013
+ * When dependencies are nested, each level wraps the error.
1014
+ * This method unwraps all layers to get the original error.
1706
1015
  */
1707
1016
  getRootCause(): unknown;
1708
1017
  }
1709
1018
  /**
1710
1019
  * Error thrown when one or more finalizers fail during container destruction.
1711
1020
  *
1712
- * This error aggregates multiple finalizer failures that occurred during
1713
- * `container.destroy()`. Even if some finalizers fail, the container cleanup
1714
- * process continues and this error contains details of all failures.
1021
+ * Even if some finalizers fail, cleanup continues for all others.
1022
+ * This error aggregates all failures.
1715
1023
  *
1716
- * @example Handling finalization errors
1024
+ * @example
1717
1025
  * ```typescript
1718
1026
  * try {
1719
1027
  * await container.destroy();
1720
1028
  * } catch (error) {
1721
1029
  * if (error instanceof DependencyFinalizationError) {
1722
- * console.error('Some finalizers failed');
1723
- * console.error('Error details:', error.detail.errors);
1030
+ * console.error('Cleanup failures:', error.getRootCauses());
1724
1031
  * }
1725
1032
  * }
1726
1033
  * ```
1727
1034
  */
1728
1035
  declare class DependencyFinalizationError extends SandlyError {
1729
1036
  private readonly errors;
1730
- /**
1731
- * @internal
1732
- * Creates a DependencyFinalizationError aggregating multiple finalizer failures.
1733
- *
1734
- * @param errors - Array of errors thrown by individual finalizers
1735
- */
1736
1037
  constructor(errors: unknown[]);
1737
1038
  /**
1738
- * Returns the root causes of the errors that occurred during finalization.
1739
- *
1740
- * @returns An array of the errors that occurred during finalization.
1741
- * You can expect at least one error in the array.
1039
+ * Returns all root cause errors from the finalization failures.
1742
1040
  */
1743
1041
  getRootCauses(): unknown[];
1744
1042
  }
1745
1043
 
1746
1044
  //#endregion
1747
- //#region src/service.d.ts
1748
- /**
1749
- * Extracts constructor parameter types from a ServiceTag.
1750
- * Only parameters that extend AnyTag are considered as dependencies.
1751
- * @internal
1752
- */
1753
- type ConstructorParams<T extends ServiceTag<TagId, unknown>> = T extends (new (...args: infer A) => unknown) ? A : never;
1754
- /**
1755
- * Helper to normalize a tag type.
1756
- * For ServiceTags, this strips away extra static properties of the class constructor,
1757
- * reducing it to the canonical ServiceTag<Id, Instance> form.
1758
- * @internal
1759
- */
1760
- type CanonicalTag<T extends AnyTag> = T extends ServiceTag<infer Id, infer Instance> ? ServiceTag<Id, Instance> : T;
1761
- /**
1762
- * Extracts only dependency tags from a constructor parameter list.
1763
- * Filters out non‑DI parameters.
1764
- *
1765
- * Example:
1766
- * [DatabaseService, Inject<typeof ConfigTag>, number]
1767
- * → typeof DatabaseService | typeof ConfigTag
1768
- * @internal
1769
- */
1770
- type ExtractConstructorDeps<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
1771
- readonly [ServiceTagIdKey]?: infer Id;
1772
- } ? Id extends TagId ? T[K] extends (new (...args: unknown[]) => infer Instance) ? ServiceTag<Id, Instance> : ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? never : ExtractInjectTag<T[K]> }[number];
1773
- /**
1774
- * Produces an ordered tuple of constructor parameters
1775
- * where dependency parameters are replaced with their tag types,
1776
- * while non‑DI parameters are preserved as‑is.
1777
- * @internal
1778
- */
1779
- type InferConstructorDepsTuple<T extends readonly unknown[]> = T extends readonly [] ? never : { [K in keyof T]: T[K] extends {
1780
- readonly [ServiceTagIdKey]?: infer Id;
1781
- } ? Id extends TagId ? T[K] extends (new (...args: unknown[]) => infer Instance) ? ServiceTag<Id, Instance> : ServiceTag<Id, T[K]> : never : ExtractInjectTag<T[K]> extends never ? T[K] : ExtractInjectTag<T[K]> };
1782
- /**
1783
- * Union of all dependency tags a ServiceTag constructor requires.
1784
- * Filters out non‑DI parameters.
1785
- */
1786
- type ServiceDependencies<T extends ServiceTag<TagId, unknown>> = ExtractConstructorDeps<ConstructorParams<T>> extends AnyTag ? ExtractConstructorDeps<ConstructorParams<T>> : never;
1787
- /**
1788
- * Ordered tuple of dependency tags (and other constructor params)
1789
- * inferred from a ServiceTag’s constructor.
1790
- */
1791
- type ServiceDepsTuple<T extends ServiceTag<TagId, unknown>> = InferConstructorDepsTuple<ConstructorParams<T>>;
1792
- /**
1793
- * Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
1794
- *
1795
- * For ServiceTag services:
1796
- * - Dependencies are automatically inferred from constructor parameters
1797
- * - The factory function must handle dependency injection by resolving dependencies from the container
1798
- *
1799
- * For ValueTag services:
1800
- * - No constructor dependencies are needed since they don't have constructors
1801
- *
1802
- * @template T - The tag representing the service (ServiceTag or ValueTag)
1803
- * @param tag - The tag (ServiceTag or ValueTag)
1804
- * @param factory - Factory function for service instantiation with container
1805
- * @returns The service layer
1806
- *
1807
- * @example Simple service without dependencies
1808
- * ```typescript
1809
- * class LoggerService extends Tag.Service('LoggerService') {
1810
- * log(message: string) { console.log(message); }
1811
- * }
1812
- *
1813
- * const loggerService = service(LoggerService, () => new LoggerService());
1814
- * ```
1815
- *
1816
- * @example Service with dependencies
1817
- * ```typescript
1818
- * class DatabaseService extends Tag.Service('DatabaseService') {
1819
- * query() { return []; }
1820
- * }
1821
- *
1822
- * class UserService extends Tag.Service('UserService') {
1823
- * constructor(private db: DatabaseService) {
1824
- * super();
1825
- * }
1826
- *
1827
- * getUsers() { return this.db.query(); }
1828
- * }
1829
- *
1830
- * const userService = service(UserService, async (ctx) =>
1831
- * new UserService(await ctx.resolve(DatabaseService))
1832
- * );
1833
- * ```
1834
- */
1835
- declare function service<T extends ServiceTag<TagId, unknown>>(tag: T, spec: DependencySpec<T, ServiceDependencies<T>>): Layer<ServiceDependencies<T>, CanonicalTag<T>>;
1836
- /**
1837
- * Specification for autoService.
1838
- * Can be either a tuple of constructor parameters or an object with dependencies and finalizer.
1839
- */
1840
- type AutoServiceSpec<T extends ServiceTag<TagId, unknown>> = ServiceDepsTuple<T> | {
1841
- dependencies: ServiceDepsTuple<T>;
1842
- cleanup?: Finalizer<TagType<T>>;
1843
- };
1844
- /**
1845
- * Creates a service layer with automatic dependency injection by inferring constructor parameters.
1846
- *
1847
- * This is a convenience function that automatically resolves constructor dependencies and passes
1848
- * both DI-managed dependencies and static values to the service constructor in the correct order.
1849
- * It eliminates the need to manually write factory functions for services with constructor dependencies.
1850
- *
1851
- * @template T - The ServiceTag representing the service class
1852
- * @param tag - The service tag (must be a ServiceTag, not a ValueTag)
1853
- * @param deps - Tuple of constructor parameters in order - mix of dependency tags and static values
1854
- * @param finalizer - Optional cleanup function called when the container is destroyed
1855
- * @returns A service layer that automatically handles dependency injection
1856
- *
1857
- * @example Simple service with dependencies
1858
- * ```typescript
1859
- * class DatabaseService extends Tag.Service('DatabaseService') {
1860
- * constructor(private url: string) {
1861
- * super();
1862
- * }
1863
- * connect() { return `Connected to ${this.url}`; }
1864
- * }
1865
- *
1866
- * class UserService extends Tag.Service('UserService') {
1867
- * constructor(private db: DatabaseService, private timeout: number) {
1868
- * super();
1869
- * }
1870
- * getUsers() { return this.db.query('SELECT * FROM users'); }
1871
- * }
1872
- *
1873
- * // Automatically inject DatabaseService and pass static timeout value
1874
- * const userService = autoService(UserService, [DatabaseService, 5000]);
1875
- * ```
1876
- *
1877
- * @example Mixed dependencies and static values
1878
- * ```typescript
1879
- * class NotificationService extends Tag.Service('NotificationService') {
1880
- * constructor(
1881
- * private logger: LoggerService,
1882
- * private apiKey: string,
1883
- * private retries: number,
1884
- * private cache: CacheService
1885
- * ) {
1886
- * super();
1887
- * }
1888
- * }
1889
- *
1890
- * // Mix of DI tags and static values in constructor order
1891
- * const notificationService = autoService(NotificationService, [
1892
- * LoggerService, // Will be resolved from container
1893
- * 'secret-api-key', // Static string value
1894
- * 3, // Static number value
1895
- * CacheService // Will be resolved from container
1896
- * ]);
1897
- * ```
1898
- *
1899
- * @example Compared to manual service creation
1900
- * ```typescript
1901
- * // Manual approach (more verbose)
1902
- * const userServiceManual = service(UserService, async (ctx) => {
1903
- * const db = await ctx.resolve(DatabaseService);
1904
- * return new UserService(db, 5000);
1905
- * });
1906
- *
1907
- * // Auto approach (concise)
1908
- * const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
1909
- * ```
1910
- *
1911
- * @example With finalizer for cleanup
1912
- * ```typescript
1913
- * class DatabaseService extends Tag.Service('DatabaseService') {
1914
- * constructor(private connectionString: string) {
1915
- * super();
1916
- * }
1917
- *
1918
- * private connection: Connection | null = null;
1919
- *
1920
- * async connect() {
1921
- * this.connection = await createConnection(this.connectionString);
1922
- * }
1923
- *
1924
- * async disconnect() {
1925
- * if (this.connection) {
1926
- * await this.connection.close();
1927
- * this.connection = null;
1928
- * }
1929
- * }
1930
- * }
1931
- *
1932
- * // Service with automatic cleanup
1933
- * const dbService = autoService(
1934
- * DatabaseService,
1935
- * {
1936
- * dependencies: ['postgresql://localhost:5432/mydb'],
1937
- * cleanup: (service) => service.disconnect() // Finalizer for cleanup
1938
- * }
1939
- * );
1940
- * ```
1941
- */
1942
- declare function autoService<T extends ServiceTag<TagId, unknown>>(tag: T, spec: AutoServiceSpec<T>): Layer<ServiceDependencies<T>, CanonicalTag<T>>;
1943
-
1944
- //#endregion
1945
- export { AnyLayer, AnyTag, CircularDependencyError, Container, ContainerDestroyedError, ContainerTags, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, DependencyLifecycle, DependencySpec, Factory, Finalizer, IContainer, Inject, InjectSource, Layer, PromiseOrValue, ResolutionContext, SandlyError, Scope, ScopedContainer, ServiceDependencies, ServiceDepsTuple, ServiceTag, Tag, TagType, UnknownDependencyError, ValueTag, autoService, constant, dependency, layer, service };
1045
+ export { AnyLayer, AnyTag, BuilderTags, CircularDependencyError, Container, ContainerBuilder, ContainerDestroyedError, ContainerTags, DependencyCreationError, DependencyFinalizationError, DependencyLifecycle, DependencySpec, ErrorDump, Factory, Finalizer, IContainer, IContainerBuilder, Layer, Layer as LayerInterface, PromiseOrValue, ResolutionContext, SandlyError, Scope, ScopedContainer, ScopedContainerBuilder, ServiceTag, Tag, TagId, TagType, UnknownDependencyError, ValueTag };