sandly 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +314 -2624
  2. package/dist/index.d.ts +645 -1557
  3. package/dist/index.js +522 -921
  4. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,1510 +1,882 @@
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
150
93
  *
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
154
- *
155
- * @example Basic usage with strings
94
+ * @example
156
95
  * ```typescript
157
- * const ApiKeyTag = Tag.of('apiKey')<string>();
158
- * const ConfigTag = Tag.of('config')<{ dbUrl: string; port: number }>();
159
- *
160
- * container
161
- * .register(ApiKeyTag, () => process.env.API_KEY!)
162
- * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost', port: 5432 }));
96
+ * const ApiKeyTag = Tag.of('ApiKey')<string>();
97
+ * const PortTag = Tag.of('Port')<number>();
98
+ * const ConfigTag = Tag.of('Config')<{ dbUrl: string; port: number }>();
163
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.
104
+ *
105
+ * For classes: uses static `Tag` property if present, otherwise `constructor.name`
106
+ * For ValueTags: uses the tag's id
164
107
  *
165
- * @example Usage with symbols
108
+ * @example
166
109
  * ```typescript
167
- * const DB_CONFIG_SYM = Symbol('database-config');
168
- * const ConfigTag = Tag.of(DB_CONFIG_SYM)<DatabaseConfig>();
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"
169
117
  *
170
- * container.register(ConfigTag, () => ({ host: 'localhost', port: 5432 }));
118
+ * const ConfigTag = Tag.of('app.config')<Config>();
119
+ * Tag.id(ConfigTag); // "app.config"
171
120
  * ```
121
+ */
122
+ id: (tag: AnyTag) => string;
123
+ /**
124
+ * Type guard to check if a value is a ServiceTag (class constructor).
172
125
  *
173
- * @example Primitive values
174
- * ```typescript
175
- * const PortTag = Tag.of('port')<number>();
176
- * const EnabledTag = Tag.of('enabled')<boolean>();
126
+ * Returns true for class declarations, class expressions, and regular functions
127
+ * (which can be used as constructors in JavaScript).
177
128
  *
178
- * container
179
- * .register(PortTag, () => 3000)
180
- * .register(EnabledTag, () => true);
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);
181
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.
182
253
  *
183
- * @example Complex objects
254
+ * @example
184
255
  * ```typescript
185
- * interface DatabaseConfig {
186
- * host: string;
187
- * port: number;
188
- * database: string;
189
- * }
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.
190
264
  *
191
- * const DbConfigTag = Tag.of('database-config')<DatabaseConfig>();
192
- * container.register(DbConfigTag, () => ({
193
- * host: 'localhost',
194
- * port: 5432,
195
- * database: 'myapp'
196
- * }));
265
+ * @example
266
+ * ```typescript
267
+ * const infraLayer = persistenceLayer.merge(loggingLayer);
197
268
  * ```
198
269
  */
199
- of: <Id extends TagId>(id: Id) => <T>() => ValueTag<Id, T>;
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: {
200
299
  /**
201
- * Creates a base class that can be extended to create service classes with dependency tags.
300
+ * Creates a layer that provides a class service with automatic dependency injection.
202
301
  *
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.
302
+ * The dependencies array must match the constructor parameters exactly (order and types).
303
+ * This is validated at compile time.
206
304
  *
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
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
210
308
  *
211
- * @example Basic service class
309
+ * @example
212
310
  * ```typescript
213
- * class UserService extends Tag.Service('UserService') {
214
- * getUsers() {
215
- * return ['alice', 'bob'];
216
- * }
311
+ * class UserService {
312
+ * constructor(private db: Database, private apiKey: string) {}
217
313
  * }
218
314
  *
219
- * container.register(UserService, () => new UserService());
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']);
220
322
  * ```
323
+ */
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>;
327
+ /**
328
+ * Creates a layer that provides a constant value.
221
329
  *
222
- * @example Service with dependencies
330
+ * @param tag - The ValueTag to register
331
+ * @param value - The value to provide
332
+ *
333
+ * @example
223
334
  * ```typescript
224
- * class DatabaseService extends Tag.Service('DatabaseService') {
225
- * query(sql: string) { return []; }
226
- * }
335
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
336
+ * const ConfigTag = Tag.of('config')<{ port: number }>();
227
337
  *
228
- * class UserRepository extends Tag.Service('UserRepository') {
229
- * constructor(private db: DatabaseService) {
230
- * super();
231
- * }
338
+ * const configLayer = Layer.value(ApiKeyTag, 'secret-key')
339
+ * .merge(Layer.value(ConfigTag, { port: 3000 }));
340
+ * ```
341
+ */
342
+ value<T extends ValueTag<TagId, unknown>>(tag: T, value: TagType<T>): Layer<never, T>;
343
+ /**
344
+ * Creates a custom layer with full control over the factory logic.
232
345
  *
233
- * findUser(id: string) {
234
- * return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
235
- * }
236
- * }
346
+ * Use this when you need custom instantiation logic that can't be expressed
347
+ * with `Layer.service()` or `Layer.value()`.
237
348
  *
238
- * container
239
- * .register(DatabaseService, () => new DatabaseService())
240
- * .register(UserRepository, async (ctx) =>
241
- * new UserRepository(await ctx.resolve(DatabaseService))
242
- * );
243
- * ```
349
+ * - `TRequires` is inferred from the `requires` array
350
+ * - `TProvides` is inferred from what `apply` adds to the builder
351
+ *
352
+ * @param options.requires - Array of tags this layer requires (use [] for no requirements)
353
+ * @param options.apply - Function that adds registrations to a builder
244
354
  *
245
- * @example With symbol identifiers
355
+ * @example
246
356
  * ```typescript
247
- * const ServiceId = Symbol('InternalService');
357
+ * // Layer with dependencies - TProvides inferred from builder.add()
358
+ * const cacheLayer = Layer.create({
359
+ * requires: [Database],
360
+ * apply: (builder) => builder
361
+ * .add(Cache, async (ctx) => {
362
+ * const db = await ctx.resolve(Database);
363
+ * return new Cache(db, { ttl: 3600 });
364
+ * })
365
+ * });
366
+ * // Type: Layer<typeof Database, typeof Cache>
248
367
  *
249
- * class InternalService extends Tag.Service(ServiceId) {
250
- * doInternalWork() { return 'work'; }
251
- * }
368
+ * // Layer with no dependencies
369
+ * const dbLayer = Layer.create({
370
+ * requires: [],
371
+ * apply: (builder) => builder.add(Database, () => new Database())
372
+ * });
373
+ * // Type: Layer<never, typeof Database>
252
374
  * ```
253
375
  */
254
- Service: <Id extends TagId>(id: Id) => ServiceTag<Id, {
255
- readonly "sandly/ServiceTagIdKey"?: Id;
256
- }>;
376
+ create<const TRequires extends readonly AnyTag[], TAllTags extends AnyTag>(options: {
377
+ requires: TRequires;
378
+ apply: (builder: IContainerBuilder<TRequires[number]>) => IContainerBuilder<TAllTags>;
379
+ }): Layer<TRequires[number], Exclude<TAllTags, TRequires[number]>>;
257
380
  /**
258
- * Extracts the string representation of a tag's identifier.
381
+ * Creates an empty layer with no requirements or provisions.
259
382
  *
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.
383
+ * @example
384
+ * ```typescript
385
+ * const baseLayer = Layer.empty()
386
+ * .merge(configLayer)
387
+ * .merge(serviceLayer);
388
+ * ```
389
+ */
390
+ empty(): Layer<never, never>;
391
+ /**
392
+ * Merges multiple layers at once.
263
393
  *
264
- * @param tag - Any valid dependency tag (value tag or service tag)
265
- * @returns String representation of the tag's identifier
394
+ * @param layers - At least 2 layers to merge
395
+ * @returns A layer combining all requirements and provisions
266
396
  *
267
397
  * @example
268
398
  * ```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"
399
+ * const infraLayer = Layer.mergeAll(
400
+ * persistenceLayer,
401
+ * messagingLayer,
402
+ * observabilityLayer
403
+ * );
274
404
  * ```
275
- *
276
- * @internal - Primarily for internal use in error messages and debugging
277
405
  */
278
- id: (tag: AnyTag) => TagId | undefined;
279
- isTag: (tag: unknown) => tag is AnyTag;
406
+ mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
407
+ /**
408
+ * Merges exactly two layers.
409
+ * Equivalent to `layer1.merge(layer2)`.
410
+ */
411
+ 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
412
  };
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
413
 
326
414
  //#endregion
327
415
  //#region src/container.d.ts
328
416
  /**
329
- * Type representing a factory function used to create dependency instances.
417
+ * Factory function that creates a dependency instance.
330
418
  *
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.
334
- *
335
- * The factory can be either synchronous (returning T directly) or asynchronous
336
- * (returning Promise<T>). The container handles both cases transparently.
419
+ * Receives a resolution context for injecting other dependencies.
420
+ * Can be synchronous or asynchronous.
337
421
  *
338
422
  * @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
- * ```
423
+ * @template TRequires - Union type of required dependencies
358
424
  */
359
425
  type Factory<T, TRequires extends AnyTag> = (ctx: ResolutionContext<TRequires>) => PromiseOrValue<T>;
360
426
  /**
361
- * Type representing a finalizer function used to clean up dependency instances.
362
- *
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
- * ```
427
+ * Cleanup function called when the container is destroyed.
385
428
  *
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
- * ```
429
+ * @template T - The type of the service instance being cleaned up
399
430
  */
400
431
  type Finalizer<T> = (instance: T) => PromiseOrValue<void>;
401
432
  /**
402
- * Type representing a complete dependency lifecycle with both factory and finalizer.
403
- *
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.
433
+ * Complete dependency lifecycle with factory and optional cleanup.
407
434
  *
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.
435
+ * Can be implemented as a class for complex lifecycle logic.
411
436
  *
412
437
  * @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
- * ```
438
+ * @template TRequires - Union type of required dependencies
490
439
  */
491
440
  interface DependencyLifecycle<T, TRequires extends AnyTag> {
492
441
  create: Factory<T, TRequires>;
493
442
  cleanup?: Finalizer<T>;
494
443
  }
495
444
  /**
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
- * ```
445
+ * Valid dependency registration: either a factory function or lifecycle object.
522
446
  */
523
447
  type DependencySpec<T extends AnyTag, TRequires extends AnyTag> = Factory<TagType<T>, TRequires> | DependencyLifecycle<TagType<T>, TRequires>;
524
448
  /**
525
- * Type representing the context available to factory functions during dependency resolution.
526
- *
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.
449
+ * Context available to factory functions during resolution.
529
450
  *
530
- * @template TTags - Union type of all dependencies available in the container
451
+ * Provides `resolve` and `resolveAll` for injecting dependencies.
531
452
  */
532
453
  type ResolutionContext<TTags extends AnyTag> = Pick<IContainer<TTags>, 'resolve' | 'resolveAll'>;
454
+ /**
455
+ * Unique symbol for container type branding.
456
+ */
533
457
  declare const ContainerTypeId: unique symbol;
534
458
  /**
535
- * Interface representing a container that can register and retrieve dependencies.
459
+ * Interface for dependency containers.
536
460
  *
537
- * @template TTags - Union type of all dependencies available in the container
461
+ * @template TTags - Union type of registered dependency tags
538
462
  */
539
463
  interface IContainer<TTags extends AnyTag = never> {
540
464
  readonly [ContainerTypeId]: {
541
465
  readonly _TTags: Contravariant<TTags>;
542
466
  };
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
467
  resolve: <T extends TTags>(tag: T) => Promise<TagType<T>>;
547
468
  resolveAll: <const T extends readonly TTags[]>(...tags: T) => Promise<{ [K in keyof T]: TagType<T[K]> }>;
469
+ use: <T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>) => Promise<R>;
548
470
  destroy(): Promise<void>;
549
471
  }
550
472
  /**
551
- * Extracts the registered tags (TTags) from a container type.
473
+ * Extracts the registered tags from a container type.
552
474
  */
553
475
  type ContainerTags<C> = C extends IContainer<infer TTags> ? TTags : never;
554
476
  /**
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.
477
+ * Common interface for container builders that support adding dependencies.
478
+ * Used by Layer to work with both ContainerBuilder and ScopedContainerBuilder.
479
+ */
480
+ interface IContainerBuilder<TTags extends AnyTag = never> {
481
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): IContainerBuilder<TTags | T>;
482
+ }
483
+ /**
484
+ * Extracts the registered tags from a builder type.
485
+ */
486
+ type BuilderTags<B> = B extends IContainerBuilder<infer TTags> ? TTags : never;
487
+ /**
488
+ * Builder for constructing immutable containers.
558
489
  *
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.
490
+ * Use `Container.builder()` to create a builder, then chain `.add()` calls
491
+ * to register dependencies, and finally call `.build()` to create the container.
562
492
  *
563
- * @template TTags - Union type of all registered dependency tags in this container
493
+ * @template TTags - Union type of registered dependency tags
564
494
  *
565
- * @example Basic usage with service tags
495
+ * @example
566
496
  * ```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);
497
+ * const container = Container.builder()
498
+ * .add(Database, () => new Database())
499
+ * .add(UserService, async (ctx) =>
500
+ * new UserService(await ctx.resolve(Database))
501
+ * )
502
+ * .build();
585
503
  * ```
504
+ */
505
+ declare class ContainerBuilder<TTags extends AnyTag = never> {
506
+ private readonly factories;
507
+ private readonly finalizers;
508
+ /**
509
+ * Registers a dependency with a factory function or lifecycle object.
510
+ *
511
+ * @param tag - The dependency tag (class or ValueTag)
512
+ * @param spec - Factory function or lifecycle object
513
+ * @returns The builder with updated type information
514
+ */
515
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ContainerBuilder<TTags | T>;
516
+ /**
517
+ * Creates an immutable container from the registered dependencies.
518
+ */
519
+ build(): Container<TTags>;
520
+ }
521
+ /**
522
+ * Type-safe dependency injection container.
586
523
  *
587
- * @example Usage with value tags
588
- * ```typescript
589
- * const ApiKeyTag = Tag.of('apiKey')<string>();
590
- * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
591
- *
592
- * const container = Container.empty()
593
- * .register(ApiKeyTag, () => process.env.API_KEY!)
594
- * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
524
+ * Containers are immutable - use `Container.builder()` to create one.
525
+ * Each dependency is created once (singleton) and cached.
595
526
  *
596
- * const apiKey = await c.resolve(ApiKeyTag);
597
- * const config = await c.resolve(ConfigTag);
598
- * ```
527
+ * @template TTags - Union type of registered dependency tags
599
528
  *
600
- * @example With finalizers for cleanup
529
+ * @example
601
530
  * ```typescript
602
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
603
- * async connect() { return; }
604
- * async disconnect() { return; }
531
+ * class Database {
532
+ * query(sql: string) { return []; }
533
+ * }
534
+ *
535
+ * class UserService {
536
+ * constructor(private db: Database) {}
537
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
605
538
  * }
606
539
  *
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
- * );
540
+ * const container = Container.builder()
541
+ * .add(Database, () => new Database())
542
+ * .add(UserService, async (ctx) =>
543
+ * new UserService(await ctx.resolve(Database))
544
+ * )
545
+ * .build();
616
546
  *
617
- * // Later...
618
- * await c.destroy(); // Calls all finalizers
547
+ * const userService = await container.resolve(UserService);
619
548
  * ```
620
549
  */
621
- declare class Container<TTags extends AnyTag> implements IContainer<TTags> {
550
+ declare class Container<TTags extends AnyTag = never> implements IContainer<TTags> {
622
551
  readonly [ContainerTypeId]: {
623
552
  readonly _TTags: Contravariant<TTags>;
624
553
  };
625
- protected constructor();
626
554
  /**
627
- * Cache of instantiated dependencies as promises.
628
- * Ensures singleton behavior and supports concurrent access.
555
+ * Cache of instantiated dependencies.
629
556
  * @internal
630
557
  */
631
558
  protected readonly cache: Map<AnyTag, Promise<unknown>>;
632
559
  /**
633
- * Factory functions for creating dependency instances.
560
+ * Factory functions for creating dependencies.
634
561
  * @internal
635
562
  */
636
563
  protected readonly factories: Map<AnyTag, Factory<unknown, TTags>>;
637
564
  /**
638
- * Finalizer functions for cleaning up dependencies when the container is destroyed.
565
+ * Cleanup functions for dependencies.
639
566
  * @internal
640
567
  */
641
568
  protected readonly finalizers: Map<AnyTag, Finalizer<any>>;
642
569
  /**
643
- * Flag indicating whether this container has been destroyed.
570
+ * Whether this container has been destroyed.
644
571
  * @internal
645
572
  */
646
573
  protected isDestroyed: boolean;
647
574
  /**
648
- * Creates a new empty container instance.
649
- * @returns A new empty Container instance with no registered dependencies.
575
+ * @internal - Use Container.builder() or Container.empty()
650
576
  */
651
- static empty(): Container<never>;
577
+ protected constructor(factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
652
578
  /**
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
- * ```
579
+ * @internal - Used by ContainerBuilder
580
+ */
581
+ static _createFromBuilder<T extends AnyTag>(factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): Container<T>;
582
+ /**
583
+ * Creates a new container builder.
681
584
  *
682
- * @example Registering with dependencies
585
+ * @example
683
586
  * ```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
- * );
587
+ * const container = Container.builder()
588
+ * .add(Database, () => new Database())
589
+ * .build();
697
590
  * ```
591
+ */
592
+ static builder(): ContainerBuilder;
593
+ /**
594
+ * Creates an empty container with no dependencies.
698
595
  *
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
- * ```
596
+ * Shorthand for `Container.builder().build()`.
597
+ */
598
+ static empty(): Container;
599
+ /**
600
+ * Creates a scoped container for hierarchical dependency management.
705
601
  *
706
- * @example Using value tags
707
- * ```typescript
708
- * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
602
+ * Scoped containers support parent/child relationships where children
603
+ * can access parent dependencies but maintain their own cache.
709
604
  *
710
- * const container = Container.empty().register(
711
- * ConfigTag,
712
- * () => ({ apiUrl: 'https://api.example.com' })
713
- * );
714
- * ```
605
+ * @param scope - Identifier for the scope (for debugging)
715
606
  *
716
- * @example With finalizer for cleanup
607
+ * @example
717
608
  * ```typescript
718
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
719
- * async connect() { return; }
720
- * async close() { return; }
721
- * }
609
+ * const appContainer = Container.scoped('app');
610
+ * // ... add app-level dependencies
722
611
  *
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
- * );
612
+ * const requestContainer = appContainer.child('request');
613
+ * // ... add request-specific dependencies
732
614
  * ```
733
615
  */
734
- register<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): Container<TTags | T>;
616
+ static scoped(scope: string | symbol): ScopedContainer;
735
617
  /**
736
- * Checks if a dependency has been registered in the container.
618
+ * Creates a container from a layer.
737
619
  *
738
- * This returns `true` if the dependency has been registered via `.register()`,
739
- * regardless of whether it has been instantiated yet.
620
+ * This is a convenience method equivalent to applying a layer to
621
+ * `Container.builder()` and building the result.
740
622
  *
741
- * @param tag - The dependency tag to check
742
- * @returns `true` if the dependency has been registered, `false` otherwise
623
+ * @param layer - A layer with no requirements (all dependencies satisfied)
743
624
  *
744
625
  * @example
745
626
  * ```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.
627
+ * const dbLayer = Layer.service(Database, []);
628
+ * const container = Container.from(dbLayer);
753
629
  *
754
- * @param tag - The dependency tag to check
755
- * @returns true if the dependency has been instantiated, false otherwise
630
+ * const db = await container.resolve(Database);
631
+ * ```
756
632
  */
757
- exists(tag: AnyTag): boolean;
633
+ static from<TProvides extends AnyTag>(layer: Layer<never, TProvides>): Container<TProvides>;
758
634
  /**
759
- * Retrieves a dependency instance from the container, creating it if necessary.
635
+ * Resolves a dependency, creating it if necessary.
760
636
  *
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.
637
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
764
638
  *
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
639
+ * @param tag - The dependency tag to resolve
640
+ * @returns Promise resolving to the dependency instance
641
+ * @throws {ContainerDestroyedError} If the container has been destroyed
642
+ * @throws {UnknownDependencyError} If any dependency is not registered
772
643
  * @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
- * ```
644
+ * @throws {DependencyCreationError} If any factory function throws an error
807
645
  */
808
646
  resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
809
647
  /**
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.
648
+ * Internal resolution with dependency chain tracking.
812
649
  * @internal
813
650
  */
814
651
  protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
815
652
  /**
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.
653
+ * Resolves multiple dependencies concurrently.
822
654
  *
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
655
+ * @param tags - The dependency tags to resolve
656
+ * @returns Promise resolving to a tuple of instances
826
657
  * @throws {ContainerDestroyedError} If the container has been destroyed
827
658
  * @throws {UnknownDependencyError} If any dependency is not registered
828
659
  * @throws {CircularDependencyError} If a circular dependency is detected
829
660
  * @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
661
  */
855
662
  resolveAll<const T extends readonly TTags[]>(...tags: T): Promise<{ [K in keyof T]: TagType<T[K]> }>;
856
663
  /**
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.
664
+ * Resolves a service, runs the callback with it, then destroys the container.
867
665
  *
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.
666
+ * This is a convenience method for the common "create, use, destroy" pattern.
667
+ * The container is always destroyed after the callback completes, even if it throws.
871
668
  *
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
- * ```
669
+ * @param tag - The dependency tag to resolve
670
+ * @param fn - Callback that receives the resolved service
671
+ * @returns Promise resolving to the callback's return value
672
+ * @throws {ContainerDestroyedError} If the container has been destroyed
673
+ * @throws {UnknownDependencyError} If the dependency is not registered
674
+ * @throws {CircularDependencyError} If a circular dependency is detected
675
+ * @throws {DependencyCreationError} If the factory function throws
676
+ * @throws {DependencyFinalizationError} If the finalizer function throws
914
677
  *
915
- * @example Handling cleanup errors
678
+ * @example
916
679
  * ```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
680
+ * const result = await container.use(UserService, (service) =>
681
+ * service.getUsers()
682
+ * );
683
+ * // Container is automatically destroyed after callback completes
925
684
  * ```
926
685
  */
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>>;
686
+ use<T extends TTags, R>(tag: T, fn: (service: TagType<T>) => PromiseOrValue<R>): Promise<R>;
980
687
  /**
981
- * Destroys this scoped container and its children, preserving the container structure for reuse.
688
+ * Destroys the container, calling all finalizers.
982
689
  *
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
690
+ * After destruction, the container cannot be used.
691
+ * Finalizers run concurrently, so there are no ordering guarantees.
692
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
693
+ * dependencies are cleaned up.
987
694
  *
988
- * Child destruction happens first to ensure dependencies don't get cleaned up
989
- * before their dependents.
695
+ * @throws {DependencyFinalizationError} If any finalizers fail
990
696
  */
991
697
  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
698
  }
1000
-
1001
- //#endregion
1002
- //#region src/layer.d.ts
1003
699
  /**
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.
700
+ * Scope identifier type.
1016
701
  */
1017
- type AnyLayer = Layer<any, any>;
1018
- /**
1019
- * The type ID for the Layer interface.
1020
- */
1021
- declare const LayerTypeId: unique symbol;
702
+ type Scope = string | symbol;
1022
703
  /**
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
- * );
704
+ * Builder for constructing scoped containers.
1072
705
  *
1073
- * // Compose layers: provide database layer to user layer
1074
- * const appLayer = userLayer.provide(databaseLayer);
1075
- * ```
706
+ * @template TTags - Union type of registered dependency tags
1076
707
  */
1077
- interface Layer<TRequires extends AnyTag, TProvides extends AnyTag> {
1078
- readonly [LayerTypeId]?: {
1079
- readonly _TRequires: Covariant<TRequires>;
1080
- readonly _TProvides: Contravariant<TProvides>;
1081
- };
708
+ declare class ScopedContainerBuilder<TTags extends AnyTag = never> {
709
+ private readonly scope;
710
+ private readonly parent;
711
+ private readonly factories;
712
+ private readonly finalizers;
713
+ constructor(scope: Scope, parent: IContainer<TTags> | null);
1082
714
  /**
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
- * ```
715
+ * Registers a dependency with a factory function or lifecycle object.
1111
716
  */
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
- * ```
717
+ add<T extends AnyTag>(tag: T, spec: DependencySpec<T, TTags>): ScopedContainerBuilder<TTags | T>;
718
+ /**
719
+ * Creates an immutable scoped container from the registered dependencies.
1203
720
  */
1204
- merge: <TOtherRequires extends AnyTag, TOtherProvides extends AnyTag>(other: Layer<TOtherRequires, TOtherProvides>) => Layer<TRequires | TOtherRequires, TProvides | TOtherProvides>;
721
+ build(): ScopedContainer<TTags>;
1205
722
  }
1206
723
  /**
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
- * }
724
+ * Scoped container for hierarchical dependency management.
1224
725
  *
1225
- * // Layer that provides DatabaseService, requires nothing
1226
- * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
1227
- * container.register(DatabaseService, () => new DatabaseService())
1228
- * );
726
+ * Supports parent/child relationships where children can access parent
727
+ * dependencies but maintain their own cache. Useful for request-scoped
728
+ * dependencies in web applications.
1229
729
  *
1230
- * // Usage
1231
- * const dbLayerInstance = databaseLayer;
1232
- * ```
730
+ * @template TTags - Union type of registered dependency tags
1233
731
  *
1234
- * @example Complex application layer structure
732
+ * @example
1235
733
  * ```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);
734
+ * // Application-level container
735
+ * const appContainer = ScopedContainer.builder('app')
736
+ * .add(Database, () => new Database())
737
+ * .build();
738
+ *
739
+ * // Request-level container (inherits from app)
740
+ * const requestContainer = appContainer.child('request')
741
+ * .add(RequestContext, () => new RequestContext())
742
+ * .build();
743
+ *
744
+ * // Can resolve both app and request dependencies
745
+ * const db = await requestContainer.resolve(Database);
746
+ * const ctx = await requestContainer.resolve(RequestContext);
1259
747
  * ```
1260
748
  */
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: {
749
+ declare class ScopedContainer<TTags extends AnyTag = never> extends Container<TTags> {
750
+ readonly scope: Scope;
751
+ private parent;
752
+ private readonly children;
753
+ /**
754
+ * @internal
755
+ */
756
+ protected constructor(scope: Scope, parent: IContainer<TTags> | null, factories: Map<AnyTag, Factory<unknown, TTags>>, finalizers: Map<AnyTag, Finalizer<any>>);
1288
757
  /**
1289
- * Creates an empty layer that provides no dependencies and requires no dependencies.
1290
- * This is useful as a base layer or for testing.
758
+ * @internal - Used by ScopedContainerBuilder
759
+ */
760
+ static _createScopedFromBuilder<T extends AnyTag>(scope: Scope, parent: IContainer<T> | null, factories: Map<AnyTag, Factory<unknown, T>>, finalizers: Map<AnyTag, Finalizer<any>>): ScopedContainer<T>;
761
+ /**
762
+ * Creates a new scoped container builder.
1291
763
  *
1292
- * @returns An empty layer that can be used as a starting point for layer composition
764
+ * @param scope - Identifier for the scope (for debugging)
1293
765
  *
1294
766
  * @example
1295
767
  * ```typescript
1296
- * import { Layer } from 'sandly';
1297
- *
1298
- * const baseLayer = Layer.empty();
1299
- * const appLayer = baseLayer
1300
- * .merge(configLayer)
1301
- * .merge(serviceLayer);
768
+ * const container = ScopedContainer.builder('app')
769
+ * .add(Database, () => new Database())
770
+ * .build();
1302
771
  * ```
1303
772
  */
1304
- empty(): Layer<never, never>;
773
+ static builder(scope: Scope): ScopedContainerBuilder;
1305
774
  /**
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
775
+ * Creates an empty scoped container with no dependencies.
776
+ */
777
+ static empty(scope: Scope): ScopedContainer;
778
+ /**
779
+ * Creates a scoped container from a layer.
1310
780
  *
1311
- * Uses the AnyLayer constraint (Layer<never, AnyTag>) which accepts any concrete layer
1312
- * through the Layer interface's variance annotations:
781
+ * This is a convenience method equivalent to applying a layer to
782
+ * `ScopedContainer.builder()` and building the result.
1313
783
  *
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
784
+ * @param scope - Identifier for the scope (for debugging)
785
+ * @param layer - A layer with no requirements (all dependencies satisfied)
1318
786
  *
1319
- * The return type correctly extracts and unions the actual requirement/provision types
1320
- * from all input layers, preserving full type safety.
787
+ * @example
788
+ * ```typescript
789
+ * const dbLayer = Layer.service(Database, []);
790
+ * const container = ScopedContainer.from('app', dbLayer);
1321
791
  *
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.
792
+ * const db = await container.resolve(Database);
793
+ * ```
794
+ */
795
+ static from<TProvides extends AnyTag>(scope: Scope, layer: Layer<never, TProvides>): ScopedContainer<TProvides>;
796
+ /**
797
+ * Resolves a dependency from this scope or parent scopes, creating it if necessary.
1325
798
  *
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
799
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
1329
800
  *
1330
- * @example Basic usage with different layer types
1331
- * ```typescript
1332
- * import { Layer } from 'sandly';
801
+ * @param tag - The dependency tag to resolve
802
+ * @returns Promise resolving to the dependency instance
803
+ * @throws {ContainerDestroyedError} If the container has been destroyed
804
+ * @throws {UnknownDependencyError} If any dependency is not registered
805
+ * @throws {CircularDependencyError} If a circular dependency is detected
806
+ * @throws {DependencyCreationError} If any factory function throws an error
807
+ */
808
+ resolve<T extends TTags>(tag: T): Promise<TagType<T>>;
809
+ /**
810
+ * Internal resolution with parent delegation.
811
+ * @internal
812
+ */
813
+ protected resolveInternal<T extends TTags>(tag: T, chain: AnyTag[]): Promise<TagType<T>>;
814
+ /**
815
+ * @internal - Used by ScopedContainerBuilder to register children
816
+ */
817
+ _registerChild(child: ScopedContainer<TTags>): void;
818
+ /**
819
+ * Creates a child container builder that inherits from this container.
1333
820
  *
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
821
+ * Use this to create a child scope and add dependencies to it.
822
+ * The child can resolve dependencies from this container.
1338
823
  *
1339
- * const infraLayer = Layer.mergeAll(dbLayer, userLayer, configLayer);
1340
- * // Type: Layer<typeof DatabaseService, typeof DatabaseService | typeof UserService | typeof ConfigService>
1341
- * ```
824
+ * @param scope - Identifier for the child scope
825
+ * @returns A new ScopedContainerBuilder for the child scope
826
+ * @throws {ContainerDestroyedError} If the container has been destroyed
1342
827
  *
1343
- * @example Equivalent to chaining .merge()
828
+ * @example
1344
829
  * ```typescript
1345
- * // These are equivalent:
1346
- * const layer1 = Layer.mergeAll(layerA, layerB, layerC);
1347
- * const layer2 = layerA.merge(layerB).merge(layerC);
830
+ * const requestContainer = appContainer.child('request')
831
+ * .add(RequestContext, () => new RequestContext())
832
+ * .build();
833
+ *
834
+ * await requestContainer.resolve(Database); // From parent
835
+ * await requestContainer.resolve(RequestContext); // From this scope
1348
836
  * ```
837
+ */
838
+ child(scope: Scope): ScopedContainerBuilder<TTags>;
839
+ /**
840
+ * Creates a child container with a layer applied.
1349
841
  *
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>(...);
842
+ * This is a convenience method combining child() + layer.apply() + build().
843
+ * Use this when you have a layer ready to apply.
1355
844
  *
1356
- * // Merge all infrastructure concerns into one layer
1357
- * const infraLayer = Layer.mergeAll(
1358
- * persistenceLayer,
1359
- * messagingLayer,
1360
- * observabilityLayer
845
+ * @param scope - Identifier for the child scope
846
+ * @param layer - Layer to apply to the child (can require parent's tags)
847
+ *
848
+ * @example
849
+ * ```typescript
850
+ * const requestContainer = appContainer.childFrom('request',
851
+ * userService
852
+ * .provide(Layer.value(TenantContext, tenantCtx))
853
+ * .provide(Layer.value(RequestId, requestId))
1361
854
  * );
1362
855
  *
1363
- * // Result type: Layer<never, DatabaseService | CacheService | MessageQueue | Logger | Metrics>
856
+ * const users = await requestContainer.resolve(UserService);
1364
857
  * ```
1365
858
  */
1366
- mergeAll<T extends readonly [AnyLayer, AnyLayer, ...AnyLayer[]]>(...layers: T): Layer<UnionOfRequires<T>, UnionOfProvides<T>>;
859
+ childFrom<TProvides extends AnyTag>(scope: Scope, layer: Layer<TTags, TProvides>): ScopedContainer<TTags | TProvides>;
1367
860
  /**
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';
861
+ * Destroys this container and all child containers.
1382
862
  *
1383
- * const dbLayer = layer<never, typeof DatabaseService>(...);
1384
- * const cacheLayer = layer<never, typeof CacheService>(...);
863
+ * Children are destroyed first to ensure proper cleanup order.
1385
864
  *
1386
- * const persistenceLayer = Layer.merge(dbLayer, cacheLayer);
1387
- * // Type: Layer<never, typeof DatabaseService | typeof CacheService>
1388
- * ```
865
+ * After destruction, the container cannot be used.
866
+ * Finalizers run concurrently, so there are no ordering guarantees.
867
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
868
+ * dependencies are cleaned up.
869
+ *
870
+ * @throws {DependencyFinalizationError} If any finalizers fail
1389
871
  */
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>;
872
+ destroy(): Promise<void>;
873
+ }
1414
874
 
1415
875
  //#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];
876
+ //#region src/errors.d.ts
1423
877
  /**
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
- * ```
878
+ * Structured error information for debugging and logging.
1503
879
  */
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
880
  type ErrorDump = {
1509
881
  name: string;
1510
882
  message: string;
@@ -1512,17 +884,20 @@ type ErrorDump = {
1512
884
  detail: Record<string, unknown>;
1513
885
  cause?: unknown;
1514
886
  };
887
+ /**
888
+ * Options for creating Sandly errors.
889
+ */
1515
890
  type SandlyErrorOptions = {
1516
891
  cause?: unknown;
1517
892
  detail?: Record<string, unknown>;
1518
893
  };
1519
894
  /**
1520
- * Base error class for all library errors.
895
+ * Base error class for all Sandly library errors.
1521
896
  *
1522
- * This extends the native Error class to provide consistent error handling
897
+ * Extends the native Error class to provide consistent error handling
1523
898
  * and structured error information across the library.
1524
899
  *
1525
- * @example Catching library errors
900
+ * @example
1526
901
  * ```typescript
1527
902
  * try {
1528
903
  * await container.resolve(SomeService);
@@ -1540,44 +915,36 @@ declare class SandlyError extends Error {
1540
915
  cause,
1541
916
  detail
1542
917
  }?: SandlyErrorOptions);
918
+ /**
919
+ * Wraps any error as a SandlyError.
920
+ */
1543
921
  static ensure(error: unknown): SandlyError;
922
+ /**
923
+ * Returns a structured representation of the error for logging.
924
+ */
1544
925
  dump(): ErrorDump;
926
+ /**
927
+ * Returns a JSON string representation of the error.
928
+ */
1545
929
  dumps(): string;
1546
930
  /**
1547
931
  * Recursively extract cause chain from any Error.
1548
- * Handles both AppError (with dump()) and plain Errors (with cause property).
1549
932
  */
1550
933
  private dumpCause;
1551
934
  }
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
935
  /**
1561
936
  * 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
937
  */
1567
938
  declare class ContainerDestroyedError extends SandlyError {}
1568
939
  /**
1569
940
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
1570
941
  *
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
942
  * @example
1576
943
  * ```typescript
1577
- * const container = Container.empty(); // Empty container
944
+ * const container = Container.builder().build(); // Empty container
1578
945
  *
1579
946
  * try {
1580
- * await c.resolve(UnregisteredService); // This will throw
947
+ * await container.resolve(UnregisteredService);
1581
948
  * } catch (error) {
1582
949
  * if (error instanceof UnknownDependencyError) {
1583
950
  * console.error('Missing dependency:', error.message);
@@ -1586,360 +953,81 @@ declare class ContainerDestroyedError extends SandlyError {}
1586
953
  * ```
1587
954
  */
1588
955
  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
956
  constructor(tag: AnyTag);
1596
957
  }
1597
958
  /**
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.
959
+ * Error thrown when a circular dependency is detected during resolution.
1603
960
  *
1604
- * @example Circular dependency scenario
961
+ * @example
1605
962
  * ```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
- *
963
+ * // ServiceA depends on ServiceB, ServiceB depends on ServiceA
1617
964
  * try {
1618
- * await c.resolve(ServiceA);
965
+ * await container.resolve(ServiceA);
1619
966
  * } catch (error) {
1620
967
  * if (error instanceof CircularDependencyError) {
1621
968
  * console.error('Circular dependency:', error.message);
1622
- * // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
969
+ * // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
1623
970
  * }
1624
971
  * }
1625
972
  * ```
1626
973
  */
1627
974
  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
975
  constructor(tag: AnyTag, dependencyChain: AnyTag[]);
1636
976
  }
1637
977
  /**
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.
978
+ * Error thrown when a dependency factory throws during instantiation.
1642
979
  *
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.
980
+ * For nested dependencies (A depends on B depends on C), use `getRootCause()`
981
+ * to unwrap all layers and get the original error.
1645
982
  *
1646
- * @example Factory throwing error
983
+ * @example
1647
984
  * ```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
985
  * try {
1655
- * await c.resolve(DatabaseService);
986
+ * await container.resolve(UserService);
1656
987
  * } catch (error) {
1657
988
  * if (error instanceof DependencyCreationError) {
1658
989
  * 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
990
  * const rootCause = error.getRootCause();
1673
- * console.error('Root cause:', rootCause); // Original error from ServiceC
991
+ * console.error('Root cause:', rootCause);
1674
992
  * }
1675
993
  * }
1676
994
  * ```
1677
995
  */
1678
996
  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
997
  constructor(tag: AnyTag, error: unknown);
1687
998
  /**
1688
999
  * Traverses the error chain to find the root cause error.
1689
1000
  *
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
- * ```
1001
+ * When dependencies are nested, each level wraps the error.
1002
+ * This method unwraps all layers to get the original error.
1706
1003
  */
1707
1004
  getRootCause(): unknown;
1708
1005
  }
1709
1006
  /**
1710
1007
  * Error thrown when one or more finalizers fail during container destruction.
1711
1008
  *
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.
1009
+ * Even if some finalizers fail, cleanup continues for all others.
1010
+ * This error aggregates all failures.
1715
1011
  *
1716
- * @example Handling finalization errors
1012
+ * @example
1717
1013
  * ```typescript
1718
1014
  * try {
1719
1015
  * await container.destroy();
1720
1016
  * } catch (error) {
1721
1017
  * if (error instanceof DependencyFinalizationError) {
1722
- * console.error('Some finalizers failed');
1723
- * console.error('Error details:', error.detail.errors);
1018
+ * console.error('Cleanup failures:', error.getRootCauses());
1724
1019
  * }
1725
1020
  * }
1726
1021
  * ```
1727
1022
  */
1728
1023
  declare class DependencyFinalizationError extends SandlyError {
1729
1024
  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
1025
  constructor(errors: unknown[]);
1737
1026
  /**
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.
1027
+ * Returns all root cause errors from the finalization failures.
1742
1028
  */
1743
1029
  getRootCauses(): unknown[];
1744
1030
  }
1745
1031
 
1746
1032
  //#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 };
1033
+ 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 };