sandly 1.0.0 → 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.js CHANGED
@@ -1,197 +1,24 @@
1
- //#region src/layer.ts
2
- /**
3
- * The type ID for the Layer interface.
4
- */
5
- const LayerTypeId = Symbol.for("sandly/Layer");
6
- /**
7
- * Creates a new dependency layer that encapsulates a set of dependency registrations.
8
- * Layers are the primary building blocks for organizing and composing dependency injection setups.
9
- *
10
- * @template TRequires - The union of dependency tags this layer requires from other layers or external setup
11
- * @template TProvides - The union of dependency tags this layer registers/provides
12
- *
13
- * @param register - Function that performs the dependency registrations. Receives a container.
14
- * @returns The layer instance.
15
- *
16
- * @example Simple layer
17
- * ```typescript
18
- * import { layer, Tag } from 'sandly';
19
- *
20
- * class DatabaseService extends Tag.Service('DatabaseService') {
21
- * constructor(private url: string = 'sqlite://memory') {}
22
- * query() { return 'data'; }
23
- * }
24
- *
25
- * // Layer that provides DatabaseService, requires nothing
26
- * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
27
- * container.register(DatabaseService, () => new DatabaseService())
28
- * );
29
- *
30
- * // Usage
31
- * const dbLayerInstance = databaseLayer;
32
- * ```
33
- *
34
- * @example Complex application layer structure
35
- * ```typescript
36
- * // Configuration layer
37
- * const configLayer = layer<never, typeof ConfigTag>((container) =>
38
- * container.register(ConfigTag, () => loadConfig())
39
- * );
40
- *
41
- * // Infrastructure layer (requires config)
42
- * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
43
- * (container) =>
44
- * container
45
- * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
46
- * .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
47
- * );
48
- *
49
- * // Service layer (requires infrastructure)
50
- * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
51
- * (container) =>
52
- * container.register(UserService, async (ctx) =>
53
- * new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
54
- * )
55
- * );
56
- *
57
- * // Compose the complete application
58
- * const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
59
- * ```
60
- */
61
- function layer(register) {
62
- const layerImpl = {
63
- register: (container) => register(container),
64
- provide(dependency$1) {
65
- return createProvidedLayer(dependency$1, layerImpl);
66
- },
67
- provideMerge(dependency$1) {
68
- return createComposedLayer(dependency$1, layerImpl);
69
- },
70
- merge(other) {
71
- return createMergedLayer(layerImpl, other);
72
- }
73
- };
74
- return layerImpl;
75
- }
1
+ //#region src/tag.ts
76
2
  /**
77
- * Internal function to create a provided layer from two layers.
78
- * This implements the `.provide()` method logic - only exposes target layer's provisions.
79
- *
3
+ * Symbol used to identify ValueTag objects at runtime.
80
4
  * @internal
81
5
  */
82
- function createProvidedLayer(dependency$1, target) {
83
- return createComposedLayer(dependency$1, target);
84
- }
6
+ const ValueTagIdKey = "sandly/ValueTagIdKey";
85
7
  /**
86
- * Internal function to create a composed layer from two layers.
87
- * This implements the `.provideMerge()` method logic - exposes both layers' provisions.
88
- *
8
+ * Symbol used to carry the phantom type for ValueTag.
89
9
  * @internal
90
10
  */
91
- function createComposedLayer(dependency$1, target) {
92
- return layer((container) => {
93
- const containerWithDependency = dependency$1.register(
94
- container
95
- // The type
96
- // IContainer<TRequires1 | TProvides1 | Exclude<TRequires2, TProvides1> | TContainer>
97
- // can be simplified to
98
- // IContainer<TRequires1 | TRequires2 | TProvides1 | TContainer>
99
- );
100
- return target.register(containerWithDependency);
101
- });
102
- }
11
+ const TagTypeKey = "sandly/TagTypeKey";
103
12
  /**
104
- * Internal function to create a merged layer from two layers.
105
- * This implements the `.merge()` method logic.
106
- *
13
+ * Helper to get an object property safely.
107
14
  * @internal
108
15
  */
109
- function createMergedLayer(layer1, layer2) {
110
- return layer((container) => {
111
- const container1 = layer1.register(container);
112
- const container2 = layer2.register(container1);
113
- return container2;
114
- });
115
- }
116
- /**
117
- * Utility object containing helper functions for working with layers.
118
- */
119
- const Layer = {
120
- empty() {
121
- return layer((container) => container);
122
- },
123
- mergeAll(...layers) {
124
- return layers.reduce((acc, layer$1) => acc.merge(layer$1));
125
- },
126
- merge(layer1, layer2) {
127
- return layer1.merge(layer2);
128
- }
129
- };
130
-
131
- //#endregion
132
- //#region src/constant.ts
133
- /**
134
- * Creates a layer that provides a constant value for a given tag.
135
- *
136
- * @param tag - The value tag to provide
137
- * @param constantValue - The constant value to provide
138
- * @returns A layer with no dependencies that provides the constant value
139
- *
140
- * @example
141
- * ```typescript
142
- * const ApiKey = Tag.of('ApiKey')<string>();
143
- * const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
144
- *
145
- * const apiKey = constant(ApiKey, 'my-secret-key');
146
- * const dbUrl = constant(DatabaseUrl, 'postgresql://localhost:5432/myapp');
147
- *
148
- * const config = Layer.merge(apiKey, dbUrl);
149
- * ```
150
- */
151
- function constant(tag, constantValue) {
152
- return layer((container) => container.register(tag, () => constantValue));
153
- }
154
-
155
- //#endregion
156
- //#region src/utils/object.ts
157
- function hasKey(obj, key) {
158
- return obj !== void 0 && obj !== null && (typeof obj === "object" || typeof obj === "function") && key in obj;
159
- }
160
- function getKey(obj, ...keys) {
161
- let current = obj;
162
- for (const key of keys) {
163
- if (!hasKey(current, key)) return void 0;
164
- current = current[key];
165
- }
166
- return current;
16
+ function getKey(obj, key) {
17
+ if (obj === null || obj === void 0) return void 0;
18
+ return obj[key];
167
19
  }
168
-
169
- //#endregion
170
- //#region src/tag.ts
171
- /**
172
- * Symbol used to identify tagged types within the dependency injection system.
173
- * This symbol is used as a property key to attach metadata to both value tags and service tags.
174
- *
175
- * Note: We can't use a symbol here becuase it produced the following TS error:
176
- * error TS4020: 'extends' clause of exported class 'NotificationService' has or is using private name 'TagIdKey'.
177
- *
178
- * @internal
179
- */
180
- const ValueTagIdKey = "sandly/ValueTagIdKey";
181
- const ServiceTagIdKey = "sandly/ServiceTagIdKey";
182
- /**
183
- * Internal string used to identify the type of a tagged type within the dependency injection system.
184
- * This string is used as a property key to attach metadata to both value tags and service tags.
185
- * It is used to carry the type of the tagged type and should not be used directly.
186
- * @internal
187
- */
188
- const TagTypeKey = "sandly/TagTypeKey";
189
20
  /**
190
- * Utility object containing factory functions for creating dependency tags.
191
- *
192
- * The Tag object provides the primary API for creating both value tags and service tags
193
- * used throughout the dependency injection system. It's the main entry point for
194
- * defining dependencies in a type-safe way.
21
+ * Utility object for creating and working with tags.
195
22
  */
196
23
  const Tag = {
197
24
  of: (id) => {
@@ -200,35 +27,35 @@ const Tag = {
200
27
  [TagTypeKey]: void 0
201
28
  });
202
29
  },
203
- Service: (id) => {
204
- class Tagged {
205
- static [ServiceTagIdKey] = id;
206
- [ServiceTagIdKey] = id;
30
+ id: (tag) => {
31
+ if (typeof tag === "function") {
32
+ const customTag = getKey(tag, "Tag");
33
+ if (customTag !== void 0) return customTag;
34
+ return tag.name || "AnonymousClass";
207
35
  }
208
- return Tagged;
36
+ return String(tag[ValueTagIdKey]);
209
37
  },
210
- id: (tag) => {
211
- return typeof tag === "function" ? tag[ServiceTagIdKey] : tag[ValueTagIdKey];
38
+ isServiceTag: (x) => {
39
+ if (typeof x !== "function") return false;
40
+ return x.prototype !== void 0;
41
+ },
42
+ isValueTag: (x) => {
43
+ return typeof x === "object" && x !== null && getKey(x, ValueTagIdKey) !== void 0;
212
44
  },
213
- isTag: (tag) => {
214
- return typeof tag === "function" ? getKey(tag, ServiceTagIdKey) !== void 0 : getKey(tag, ValueTagIdKey) !== void 0;
45
+ isTag: (x) => {
46
+ return Tag.isServiceTag(x) || Tag.isValueTag(x);
215
47
  }
216
48
  };
217
- /**
218
- * String used to store the original ValueTag in Inject<T> types.
219
- * This prevents property name collisions while allowing type-level extraction.
220
- */
221
- const InjectSource = "sandly/InjectSource";
222
49
 
223
50
  //#endregion
224
51
  //#region src/errors.ts
225
52
  /**
226
- * Base error class for all library errors.
53
+ * Base error class for all Sandly library errors.
227
54
  *
228
- * This extends the native Error class to provide consistent error handling
55
+ * Extends the native Error class to provide consistent error handling
229
56
  * and structured error information across the library.
230
57
  *
231
- * @example Catching library errors
58
+ * @example
232
59
  * ```typescript
233
60
  * try {
234
61
  * await container.resolve(SomeService);
@@ -248,9 +75,15 @@ var SandlyError = class SandlyError extends Error {
248
75
  this.detail = detail;
249
76
  if (cause instanceof Error && cause.stack !== void 0) this.stack = `${this.stack ?? ""}\nCaused by: ${cause.stack}`;
250
77
  }
78
+ /**
79
+ * Wraps any error as a SandlyError.
80
+ */
251
81
  static ensure(error) {
252
82
  return error instanceof SandlyError ? error : new SandlyError("An unknown error occurred", { cause: error });
253
83
  }
84
+ /**
85
+ * Returns a structured representation of the error for logging.
86
+ */
254
87
  dump() {
255
88
  return {
256
89
  name: this.name,
@@ -260,12 +93,14 @@ var SandlyError = class SandlyError extends Error {
260
93
  cause: this.dumpCause(this.cause)
261
94
  };
262
95
  }
96
+ /**
97
+ * Returns a JSON string representation of the error.
98
+ */
263
99
  dumps() {
264
100
  return JSON.stringify(this.dump());
265
101
  }
266
102
  /**
267
103
  * Recursively extract cause chain from any Error.
268
- * Handles both AppError (with dump()) and plain Errors (with cause property).
269
104
  */
270
105
  dumpCause(cause) {
271
106
  if (cause instanceof SandlyError) return cause.dump();
@@ -281,34 +116,18 @@ var SandlyError = class SandlyError extends Error {
281
116
  }
282
117
  };
283
118
  /**
284
- * Error thrown when attempting to register a dependency that has already been instantiated.
285
- *
286
- * This error occurs when calling `container.register()` for a tag that has already been instantiated.
287
- * Registration must happen before any instantiation occurs, as cached instances would still be used
288
- * by existing dependencies.
289
- */
290
- var DependencyAlreadyInstantiatedError = class extends SandlyError {};
291
- /**
292
119
  * Error thrown when attempting to use a container that has been destroyed.
293
- *
294
- * This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
295
- * on a container that has already been destroyed. It indicates a programming error where the container
296
- * is being used after it has been destroyed.
297
120
  */
298
121
  var ContainerDestroyedError = class extends SandlyError {};
299
122
  /**
300
123
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
301
124
  *
302
- * This error occurs when calling `container.resolve(Tag)` for a tag that was never
303
- * registered via `container.register()`. It indicates a programming error where
304
- * the dependency setup is incomplete.
305
- *
306
125
  * @example
307
126
  * ```typescript
308
- * const container = Container.empty(); // Empty container
127
+ * const container = Container.builder().build(); // Empty container
309
128
  *
310
129
  * try {
311
- * await c.resolve(UnregisteredService); // This will throw
130
+ * await container.resolve(UnregisteredService);
312
131
  * } catch (error) {
313
132
  * if (error instanceof UnknownDependencyError) {
314
133
  * console.error('Missing dependency:', error.message);
@@ -317,113 +136,57 @@ var ContainerDestroyedError = class extends SandlyError {};
317
136
  * ```
318
137
  */
319
138
  var UnknownDependencyError = class extends SandlyError {
320
- /**
321
- * @internal
322
- * Creates an UnknownDependencyError for the given tag.
323
- *
324
- * @param tag - The dependency tag that wasn't found
325
- */
326
139
  constructor(tag) {
327
- super(`No factory registered for dependency ${String(Tag.id(tag))}`);
140
+ super(`No factory registered for dependency "${Tag.id(tag)}"`);
328
141
  }
329
142
  };
330
143
  /**
331
- * Error thrown when a circular dependency is detected during dependency resolution.
332
- *
333
- * This occurs when service A depends on service B, which depends on service A (directly
334
- * or through a chain of dependencies). The error includes the full dependency chain
335
- * to help identify the circular reference.
144
+ * Error thrown when a circular dependency is detected during resolution.
336
145
  *
337
- * @example Circular dependency scenario
146
+ * @example
338
147
  * ```typescript
339
- * class ServiceA extends Tag.Service('ServiceA') {}
340
- * class ServiceB extends Tag.Service('ServiceB') {}
341
- *
342
- * const container = Container.empty()
343
- * .register(ServiceA, async (ctx) =>
344
- * new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
345
- * )
346
- * .register(ServiceB, async (ctx) =>
347
- * new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
348
- * );
349
- *
148
+ * // ServiceA depends on ServiceB, ServiceB depends on ServiceA
350
149
  * try {
351
- * await c.resolve(ServiceA);
150
+ * await container.resolve(ServiceA);
352
151
  * } catch (error) {
353
152
  * if (error instanceof CircularDependencyError) {
354
153
  * console.error('Circular dependency:', error.message);
355
- * // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
154
+ * // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
356
155
  * }
357
156
  * }
358
157
  * ```
359
158
  */
360
159
  var CircularDependencyError = class extends SandlyError {
361
- /**
362
- * @internal
363
- * Creates a CircularDependencyError with the dependency chain information.
364
- *
365
- * @param tag - The tag where the circular dependency was detected
366
- * @param dependencyChain - The chain of dependencies that led to the circular reference
367
- */
368
160
  constructor(tag, dependencyChain) {
369
161
  const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
370
- super(`Circular dependency detected for ${String(Tag.id(tag))}: ${chain} -> ${String(Tag.id(tag))}`, { detail: {
162
+ super(`Circular dependency detected for "${Tag.id(tag)}": ${chain} -> ${Tag.id(tag)}`, { detail: {
371
163
  tag: Tag.id(tag),
372
164
  dependencyChain: dependencyChain.map((t) => Tag.id(t))
373
165
  } });
374
166
  }
375
167
  };
376
168
  /**
377
- * Error thrown when a dependency factory function throws an error during instantiation.
169
+ * Error thrown when a dependency factory throws during instantiation.
378
170
  *
379
- * This wraps the original error with additional context about which dependency
380
- * failed to be created. The original error is preserved as the `cause` property.
171
+ * For nested dependencies (A depends on B depends on C), use `getRootCause()`
172
+ * to unwrap all layers and get the original error.
381
173
  *
382
- * When dependencies are nested (A depends on B depends on C), and C's factory throws,
383
- * you get nested DependencyCreationErrors. Use `getRootCause()` to get the original error.
384
- *
385
- * @example Factory throwing error
174
+ * @example
386
175
  * ```typescript
387
- * class DatabaseService extends Tag.Service('DatabaseService') {}
388
- *
389
- * const container = Container.empty().register(DatabaseService, () => {
390
- * throw new Error('Database connection failed');
391
- * });
392
- *
393
176
  * try {
394
- * await c.resolve(DatabaseService);
177
+ * await container.resolve(UserService);
395
178
  * } catch (error) {
396
179
  * if (error instanceof DependencyCreationError) {
397
180
  * console.error('Failed to create:', error.message);
398
- * console.error('Original error:', error.cause);
399
- * }
400
- * }
401
- * ```
402
- *
403
- * @example Getting root cause from nested errors
404
- * ```typescript
405
- * // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
406
- * try {
407
- * await container.resolve(ServiceA);
408
- * } catch (error) {
409
- * if (error instanceof DependencyCreationError) {
410
- * console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
411
181
  * const rootCause = error.getRootCause();
412
- * console.error('Root cause:', rootCause); // Original error from ServiceC
182
+ * console.error('Root cause:', rootCause);
413
183
  * }
414
184
  * }
415
185
  * ```
416
186
  */
417
187
  var DependencyCreationError = class DependencyCreationError extends SandlyError {
418
- /**
419
- * @internal
420
- * Creates a DependencyCreationError wrapping the original factory error.
421
- *
422
- * @param tag - The tag of the dependency that failed to be created
423
- * @param error - The original error thrown by the factory function
424
- */
425
188
  constructor(tag, error) {
426
- super(`Error creating instance of ${String(Tag.id(tag))}`, {
189
+ super(`Error creating instance of "${Tag.id(tag)}"`, {
427
190
  cause: error,
428
191
  detail: { tag: Tag.id(tag) }
429
192
  });
@@ -431,22 +194,8 @@ var DependencyCreationError = class DependencyCreationError extends SandlyError
431
194
  /**
432
195
  * Traverses the error chain to find the root cause error.
433
196
  *
434
- * When dependencies are nested, each level wraps the error in a DependencyCreationError.
435
- * This method unwraps all the layers to get to the original error that started the failure.
436
- *
437
- * @returns The root cause error (not a DependencyCreationError unless that's the only error)
438
- *
439
- * @example
440
- * ```typescript
441
- * try {
442
- * await container.resolve(UserService);
443
- * } catch (error) {
444
- * if (error instanceof DependencyCreationError) {
445
- * const rootCause = error.getRootCause();
446
- * console.error('Root cause:', rootCause);
447
- * }
448
- * }
449
- * ```
197
+ * When dependencies are nested, each level wraps the error.
198
+ * This method unwraps all layers to get the original error.
450
199
  */
451
200
  getRootCause() {
452
201
  let current = this.cause;
@@ -457,42 +206,31 @@ var DependencyCreationError = class DependencyCreationError extends SandlyError
457
206
  /**
458
207
  * Error thrown when one or more finalizers fail during container destruction.
459
208
  *
460
- * This error aggregates multiple finalizer failures that occurred during
461
- * `container.destroy()`. Even if some finalizers fail, the container cleanup
462
- * process continues and this error contains details of all failures.
209
+ * Even if some finalizers fail, cleanup continues for all others.
210
+ * This error aggregates all failures.
463
211
  *
464
- * @example Handling finalization errors
212
+ * @example
465
213
  * ```typescript
466
214
  * try {
467
215
  * await container.destroy();
468
216
  * } catch (error) {
469
217
  * if (error instanceof DependencyFinalizationError) {
470
- * console.error('Some finalizers failed');
471
- * console.error('Error details:', error.detail.errors);
218
+ * console.error('Cleanup failures:', error.getRootCauses());
472
219
  * }
473
220
  * }
474
221
  * ```
475
222
  */
476
223
  var DependencyFinalizationError = class extends SandlyError {
477
- /**
478
- * @internal
479
- * Creates a DependencyFinalizationError aggregating multiple finalizer failures.
480
- *
481
- * @param errors - Array of errors thrown by individual finalizers
482
- */
483
224
  constructor(errors) {
484
- const lambdaErrors = errors.map((error) => SandlyError.ensure(error));
485
- super("Error destroying dependency container", {
225
+ const sandlyErrors = errors.map((error) => SandlyError.ensure(error));
226
+ super("Error destroying container", {
486
227
  cause: errors[0],
487
- detail: { errors: lambdaErrors.map((error) => error.dump()) }
228
+ detail: { errors: sandlyErrors.map((error) => error.dump()) }
488
229
  });
489
230
  this.errors = errors;
490
231
  }
491
232
  /**
492
- * Returns the root causes of the errors that occurred during finalization.
493
- *
494
- * @returns An array of the errors that occurred during finalization.
495
- * You can expect at least one error in the array.
233
+ * Returns all root cause errors from the finalization failures.
496
234
  */
497
235
  getRootCauses() {
498
236
  return this.errors;
@@ -519,283 +257,199 @@ var ResolutionContextImpl = class {
519
257
  return results;
520
258
  }
521
259
  };
260
+ /**
261
+ * Unique symbol for container type branding.
262
+ */
522
263
  const ContainerTypeId = Symbol.for("sandly/Container");
523
264
  /**
524
- * A type-safe dependency injection container that manages service instantiation,
525
- * caching, and lifecycle management with support for async dependencies and
526
- * circular dependency detection.
265
+ * Builder for constructing immutable containers.
527
266
  *
528
- * The container maintains complete type safety by tracking registered dependencies
529
- * at the type level, ensuring that only registered dependencies can be retrieved
530
- * and preventing runtime errors.
267
+ * Use `Container.builder()` to create a builder, then chain `.add()` calls
268
+ * to register dependencies, and finally call `.build()` to create the container.
531
269
  *
532
- * @template TTags - Union type of all registered dependency tags in this container
270
+ * @template TTags - Union type of registered dependency tags
533
271
  *
534
- * @example Basic usage with service tags
272
+ * @example
535
273
  * ```typescript
536
- * import { container, Tag } from 'sandly';
537
- *
538
- * class DatabaseService extends Tag.Service('DatabaseService') {
539
- * query() { return 'data'; }
540
- * }
541
- *
542
- * class UserService extends Tag.Service('UserService') {
543
- * constructor(private db: DatabaseService) {}
544
- * getUser() { return this.db.query(); }
545
- * }
546
- *
547
- * const container = Container.empty()
548
- * .register(DatabaseService, () => new DatabaseService())
549
- * .register(UserService, async (ctx) =>
550
- * new UserService(await ctx.resolve(DatabaseService))
551
- * );
552
- *
553
- * const userService = await c.resolve(UserService);
274
+ * const container = Container.builder()
275
+ * .add(Database, () => new Database())
276
+ * .add(UserService, async (ctx) =>
277
+ * new UserService(await ctx.resolve(Database))
278
+ * )
279
+ * .build();
554
280
  * ```
281
+ */
282
+ var ContainerBuilder = class {
283
+ factories = new Map();
284
+ finalizers = new Map();
285
+ /**
286
+ * Registers a dependency with a factory function or lifecycle object.
287
+ *
288
+ * @param tag - The dependency tag (class or ValueTag)
289
+ * @param spec - Factory function or lifecycle object
290
+ * @returns The builder with updated type information
291
+ */
292
+ add(tag, spec) {
293
+ if (typeof spec === "function") {
294
+ this.factories.set(tag, spec);
295
+ this.finalizers.delete(tag);
296
+ } else {
297
+ this.factories.set(tag, spec.create.bind(spec));
298
+ if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
299
+ else this.finalizers.delete(tag);
300
+ }
301
+ return this;
302
+ }
303
+ /**
304
+ * Creates an immutable container from the registered dependencies.
305
+ */
306
+ build() {
307
+ return Container._createFromBuilder(this.factories, this.finalizers);
308
+ }
309
+ };
310
+ /**
311
+ * Type-safe dependency injection container.
555
312
  *
556
- * @example Usage with value tags
557
- * ```typescript
558
- * const ApiKeyTag = Tag.of('apiKey')<string>();
559
- * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
560
- *
561
- * const container = Container.empty()
562
- * .register(ApiKeyTag, () => process.env.API_KEY!)
563
- * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
313
+ * Containers are immutable - use `Container.builder()` to create one.
314
+ * Each dependency is created once (singleton) and cached.
564
315
  *
565
- * const apiKey = await c.resolve(ApiKeyTag);
566
- * const config = await c.resolve(ConfigTag);
567
- * ```
316
+ * @template TTags - Union type of registered dependency tags
568
317
  *
569
- * @example With finalizers for cleanup
318
+ * @example
570
319
  * ```typescript
571
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
572
- * async connect() { return; }
573
- * async disconnect() { return; }
320
+ * class Database {
321
+ * query(sql: string) { return []; }
574
322
  * }
575
323
  *
576
- * const container = Container.empty().register(
577
- * DatabaseConnection,
578
- * async () => {
579
- * const conn = new DatabaseConnection();
580
- * await conn.connect();
581
- * return conn;
582
- * },
583
- * async (conn) => conn.disconnect() // Finalizer for cleanup
584
- * );
324
+ * class UserService {
325
+ * constructor(private db: Database) {}
326
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
327
+ * }
328
+ *
329
+ * const container = Container.builder()
330
+ * .add(Database, () => new Database())
331
+ * .add(UserService, async (ctx) =>
332
+ * new UserService(await ctx.resolve(Database))
333
+ * )
334
+ * .build();
585
335
  *
586
- * // Later...
587
- * await c.destroy(); // Calls all finalizers
336
+ * const userService = await container.resolve(UserService);
588
337
  * ```
589
338
  */
590
339
  var Container = class Container {
591
340
  [ContainerTypeId];
592
- constructor() {}
593
341
  /**
594
- * Cache of instantiated dependencies as promises.
595
- * Ensures singleton behavior and supports concurrent access.
342
+ * Cache of instantiated dependencies.
596
343
  * @internal
597
344
  */
598
345
  cache = new Map();
599
346
  /**
600
- * Factory functions for creating dependency instances.
347
+ * Factory functions for creating dependencies.
601
348
  * @internal
602
349
  */
603
- factories = new Map();
350
+ factories;
604
351
  /**
605
- * Finalizer functions for cleaning up dependencies when the container is destroyed.
352
+ * Cleanup functions for dependencies.
606
353
  * @internal
607
354
  */
608
- finalizers = new Map();
355
+ finalizers;
609
356
  /**
610
- * Flag indicating whether this container has been destroyed.
357
+ * Whether this container has been destroyed.
611
358
  * @internal
612
359
  */
613
360
  isDestroyed = false;
614
361
  /**
615
- * Creates a new empty container instance.
616
- * @returns A new empty Container instance with no registered dependencies.
362
+ * @internal - Use Container.builder() or Container.empty()
617
363
  */
618
- static empty() {
619
- return new Container();
364
+ constructor(factories, finalizers) {
365
+ this.factories = factories;
366
+ this.finalizers = finalizers;
620
367
  }
621
368
  /**
622
- * Registers a dependency in the container with a factory function and optional finalizer.
623
- *
624
- * The factory function receives the current container instance and must return the
625
- * service instance (or a Promise of it). The container tracks the registration at
626
- * the type level, ensuring type safety for subsequent `.resolve()` calls.
627
- *
628
- * If a dependency is already registered, this method will override it unless the
629
- * dependency has already been instantiated, in which case it will throw an error.
630
- *
631
- * @template T - The dependency tag being registered
632
- * @param tag - The dependency tag (class or value tag)
633
- * @param factory - Function that creates the service instance, receives container for dependency injection
634
- * @param finalizer - Optional cleanup function called when container is destroyed
635
- * @returns A new container instance with the dependency registered
636
- * @throws {ContainerDestroyedError} If the container has been destroyed
637
- * @throws {Error} If the dependency has already been instantiated
638
- *
639
- * @example Registering a simple service
640
- * ```typescript
641
- * class LoggerService extends Tag.Service('LoggerService') {
642
- * log(message: string) { console.log(message); }
643
- * }
644
- *
645
- * const container = Container.empty().register(
646
- * LoggerService,
647
- * () => new LoggerService()
648
- * );
649
- * ```
650
- *
651
- * @example Registering with dependencies
652
- * ```typescript
653
- * class UserService extends Tag.Service('UserService') {
654
- * constructor(private db: DatabaseService, private logger: LoggerService) {}
655
- * }
656
- *
657
- * const container = Container.empty()
658
- * .register(DatabaseService, () => new DatabaseService())
659
- * .register(LoggerService, () => new LoggerService())
660
- * .register(UserService, async (ctx) =>
661
- * new UserService(
662
- * await ctx.resolve(DatabaseService),
663
- * await ctx.resolve(LoggerService)
664
- * )
665
- * );
666
- * ```
667
- *
668
- * @example Overriding a dependency
669
- * ```typescript
670
- * const container = Container.empty()
671
- * .register(DatabaseService, () => new DatabaseService())
672
- * .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
673
- * ```
674
- *
675
- * @example Using value tags
676
- * ```typescript
677
- * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
678
- *
679
- * const container = Container.empty().register(
680
- * ConfigTag,
681
- * () => ({ apiUrl: 'https://api.example.com' })
682
- * );
683
- * ```
684
- *
685
- * @example With finalizer for cleanup
686
- * ```typescript
687
- * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
688
- * async connect() { return; }
689
- * async close() { return; }
690
- * }
691
- *
692
- * const container = Container.empty().register(
693
- * DatabaseConnection,
694
- * async () => {
695
- * const conn = new DatabaseConnection();
696
- * await conn.connect();
697
- * return conn;
698
- * },
699
- * (conn) => conn.close() // Called during container.destroy()
700
- * );
701
- * ```
369
+ * @internal - Used by ContainerBuilder
702
370
  */
703
- register(tag, spec) {
704
- if (this.isDestroyed) throw new ContainerDestroyedError("Cannot register dependencies on a destroyed container");
705
- if (this.has(tag) && this.exists(tag)) throw new DependencyAlreadyInstantiatedError(`Cannot register dependency ${String(Tag.id(tag))} - it has already been instantiated. Registration must happen before any instantiation occurs, as cached instances would still be used by existing dependencies.`);
706
- if (typeof spec === "function") {
707
- this.factories.set(tag, spec);
708
- this.finalizers.delete(tag);
709
- } else {
710
- this.factories.set(tag, spec.create.bind(spec));
711
- if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
712
- else this.finalizers.delete(tag);
713
- }
714
- return this;
371
+ static _createFromBuilder(factories, finalizers) {
372
+ return new Container(factories, finalizers);
715
373
  }
716
374
  /**
717
- * Checks if a dependency has been registered in the container.
718
- *
719
- * This returns `true` if the dependency has been registered via `.register()`,
720
- * regardless of whether it has been instantiated yet.
721
- *
722
- * @param tag - The dependency tag to check
723
- * @returns `true` if the dependency has been registered, `false` otherwise
375
+ * Creates a new container builder.
724
376
  *
725
377
  * @example
726
378
  * ```typescript
727
- * const container = Container.empty().register(DatabaseService, () => new DatabaseService());
728
- * console.log(c.has(DatabaseService)); // true
379
+ * const container = Container.builder()
380
+ * .add(Database, () => new Database())
381
+ * .build();
729
382
  * ```
730
383
  */
731
- has(tag) {
732
- return this.factories.has(tag);
384
+ static builder() {
385
+ return new ContainerBuilder();
733
386
  }
734
387
  /**
735
- * Checks if a dependency has been instantiated (cached) in the container.
388
+ * Creates an empty container with no dependencies.
736
389
  *
737
- * @param tag - The dependency tag to check
738
- * @returns true if the dependency has been instantiated, false otherwise
390
+ * Shorthand for `Container.builder().build()`.
739
391
  */
740
- exists(tag) {
741
- return this.cache.has(tag);
392
+ static empty() {
393
+ return Container.builder().build();
742
394
  }
743
395
  /**
744
- * Retrieves a dependency instance from the container, creating it if necessary.
396
+ * Creates a scoped container for hierarchical dependency management.
745
397
  *
746
- * This method ensures singleton behavior - each dependency is created only once
747
- * and cached for subsequent calls. The method is async-safe and handles concurrent
748
- * requests for the same dependency correctly.
398
+ * Scoped containers support parent/child relationships where children
399
+ * can access parent dependencies but maintain their own cache.
749
400
  *
750
- * The method performs circular dependency detection by tracking the resolution chain
751
- * through the resolution context.
401
+ * @param scope - Identifier for the scope (for debugging)
752
402
  *
753
- * @template T - The dependency tag type (must be registered in this container)
754
- * @param tag - The dependency tag to retrieve
755
- * @returns Promise resolving to the service instance
756
- * @throws {UnknownDependencyError} If the dependency is not registered
757
- * @throws {CircularDependencyError} If a circular dependency is detected
758
- * @throws {DependencyCreationError} If the factory function throws an error
759
- *
760
- * @example Basic usage
403
+ * @example
761
404
  * ```typescript
762
- * const container = Container.empty()
763
- * .register(DatabaseService, () => new DatabaseService());
405
+ * const appContainer = Container.scoped('app');
406
+ * // ... add app-level dependencies
764
407
  *
765
- * const db = await c.resolve(DatabaseService);
766
- * db.query('SELECT * FROM users');
408
+ * const requestContainer = appContainer.child('request');
409
+ * // ... add request-specific dependencies
767
410
  * ```
411
+ */
412
+ static scoped(scope) {
413
+ return ScopedContainer.empty(scope);
414
+ }
415
+ /**
416
+ * Creates a container from a layer.
768
417
  *
769
- * @example Concurrent access (singleton behavior)
770
- * ```typescript
771
- * // All three calls will receive the same instance
772
- * const [db1, db2, db3] = await Promise.all([
773
- * c.resolve(DatabaseService),
774
- * c.resolve(DatabaseService),
775
- * c.resolve(DatabaseService)
776
- * ]);
418
+ * This is a convenience method equivalent to applying a layer to
419
+ * `Container.builder()` and building the result.
777
420
  *
778
- * console.log(db1 === db2 === db3); // true
779
- * ```
421
+ * @param layer - A layer with no requirements (all dependencies satisfied)
780
422
  *
781
- * @example Dependency injection in factories
423
+ * @example
782
424
  * ```typescript
783
- * const container = Container.empty()
784
- * .register(DatabaseService, () => new DatabaseService())
785
- * .register(UserService, async (ctx) => {
786
- * const db = await ctx.resolve(DatabaseService);
787
- * return new UserService(db);
788
- * });
425
+ * const dbLayer = Layer.service(Database, []);
426
+ * const container = Container.from(dbLayer);
789
427
  *
790
- * const userService = await c.resolve(UserService);
428
+ * const db = await container.resolve(Database);
791
429
  * ```
792
430
  */
431
+ static from(layer) {
432
+ const builder = Container.builder();
433
+ const resultBuilder = layer.apply(builder);
434
+ return resultBuilder.build();
435
+ }
436
+ /**
437
+ * Resolves a dependency, creating it if necessary.
438
+ *
439
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
440
+ *
441
+ * @param tag - The dependency tag to resolve
442
+ * @returns Promise resolving to the dependency instance
443
+ * @throws {ContainerDestroyedError} If the container has been destroyed
444
+ * @throws {UnknownDependencyError} If any dependency is not registered
445
+ * @throws {CircularDependencyError} If a circular dependency is detected
446
+ * @throws {DependencyCreationError} If any factory function throws an error
447
+ */
793
448
  async resolve(tag) {
794
449
  return this.resolveInternal(tag, []);
795
450
  }
796
451
  /**
797
- * Internal resolution method that tracks the dependency chain for circular dependency detection.
798
- * Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
452
+ * Internal resolution with dependency chain tracking.
799
453
  * @internal
800
454
  */
801
455
  resolveInternal(tag, chain) {
@@ -806,7 +460,7 @@ var Container = class Container {
806
460
  const factory = this.factories.get(tag);
807
461
  if (factory === void 0) throw new UnknownDependencyError(tag);
808
462
  const newChain = [...chain, tag];
809
- const context = new ResolutionContextImpl((tag$1) => this.resolveInternal(tag$1, newChain));
463
+ const context = new ResolutionContextImpl((t) => this.resolveInternal(t, newChain));
810
464
  const instancePromise = (async () => {
811
465
  try {
812
466
  const instance = await factory(context);
@@ -822,44 +476,14 @@ var Container = class Container {
822
476
  return instancePromise;
823
477
  }
824
478
  /**
825
- * Resolves multiple dependencies concurrently using Promise.all.
479
+ * Resolves multiple dependencies concurrently.
826
480
  *
827
- * This method takes a variable number of dependency tags and resolves all of them concurrently,
828
- * returning a tuple with the resolved instances in the same order as the input tags.
829
- * The method maintains all the same guarantees as the individual resolve method:
830
- * singleton behavior, circular dependency detection, and proper error handling.
831
- *
832
- * @template T - The tuple type of dependency tags to resolve
833
- * @param tags - Variable number of dependency tags to resolve
834
- * @returns Promise resolving to a tuple of service instances in the same order
481
+ * @param tags - The dependency tags to resolve
482
+ * @returns Promise resolving to a tuple of instances
835
483
  * @throws {ContainerDestroyedError} If the container has been destroyed
836
484
  * @throws {UnknownDependencyError} If any dependency is not registered
837
485
  * @throws {CircularDependencyError} If a circular dependency is detected
838
486
  * @throws {DependencyCreationError} If any factory function throws an error
839
- *
840
- * @example Basic usage
841
- * ```typescript
842
- * const container = Container.empty()
843
- * .register(DatabaseService, () => new DatabaseService())
844
- * .register(LoggerService, () => new LoggerService());
845
- *
846
- * const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
847
- * ```
848
- *
849
- * @example Mixed tag types
850
- * ```typescript
851
- * const ApiKeyTag = Tag.of('apiKey')<string>();
852
- * const container = Container.empty()
853
- * .register(ApiKeyTag, () => 'secret-key')
854
- * .register(UserService, () => new UserService());
855
- *
856
- * const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
857
- * ```
858
- *
859
- * @example Empty array
860
- * ```typescript
861
- * const results = await c.resolveAll(); // Returns empty array
862
- * ```
863
487
  */
864
488
  async resolveAll(...tags) {
865
489
  if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
@@ -868,75 +492,45 @@ var Container = class Container {
868
492
  return results;
869
493
  }
870
494
  /**
871
- * Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
495
+ * Resolves a service, runs the callback with it, then destroys the container.
872
496
  *
873
- * **Important: After calling destroy(), the container becomes permanently unusable.**
874
- * Any subsequent calls to register(), get(), or destroy() will throw a DependencyFinalizationError.
875
- * This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
876
- *
877
- * All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
878
- * for maximum cleanup performance.
879
- * If any finalizers fail, all errors are collected and a DependencyFinalizationError
880
- * is thrown containing details of all failures.
881
- *
882
- * **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
883
- * Services should be designed to handle cleanup gracefully regardless of the order in which their
884
- * dependencies are cleaned up.
497
+ * This is a convenience method for the common "create, use, destroy" pattern.
498
+ * The container is always destroyed after the callback completes, even if it throws.
885
499
  *
886
- * @returns Promise that resolves when all cleanup is complete
887
- * @throws {DependencyFinalizationError} If any finalizers fail during cleanup
500
+ * @param tag - The dependency tag to resolve
501
+ * @param fn - Callback that receives the resolved service
502
+ * @returns Promise resolving to the callback's return value
503
+ * @throws {ContainerDestroyedError} If the container has been destroyed
504
+ * @throws {UnknownDependencyError} If the dependency is not registered
505
+ * @throws {CircularDependencyError} If a circular dependency is detected
506
+ * @throws {DependencyCreationError} If the factory function throws
507
+ * @throws {DependencyFinalizationError} If the finalizer function throws
888
508
  *
889
- * @example Basic cleanup
509
+ * @example
890
510
  * ```typescript
891
- * const container = Container.empty()
892
- * .register(DatabaseConnection,
893
- * async () => {
894
- * const conn = new DatabaseConnection();
895
- * await conn.connect();
896
- * return conn;
897
- * },
898
- * (conn) => conn.disconnect() // Finalizer
899
- * );
900
- *
901
- * const db = await c.resolve(DatabaseConnection);
902
- * await c.destroy(); // Calls conn.disconnect(), container becomes unusable
903
- *
904
- * // This will throw an error
905
- * try {
906
- * await c.resolve(DatabaseConnection);
907
- * } catch (error) {
908
- * console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
909
- * }
511
+ * const result = await container.use(UserService, (service) =>
512
+ * service.getUsers()
513
+ * );
514
+ * // Container is automatically destroyed after callback completes
910
515
  * ```
516
+ */
517
+ async use(tag, fn) {
518
+ try {
519
+ const service = await this.resolve(tag);
520
+ return await fn(service);
521
+ } finally {
522
+ await this.destroy();
523
+ }
524
+ }
525
+ /**
526
+ * Destroys the container, calling all finalizers.
911
527
  *
912
- * @example Application shutdown
913
- * ```typescript
914
- * const appContainer Container.empty
915
- * .register(DatabaseService, () => new DatabaseService())
916
- * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
917
- *
918
- * // During application shutdown
919
- * process.on('SIGTERM', async () => {
920
- * try {
921
- * await appContainer.destroy(); // Clean shutdown of all services
922
- * } catch (error) {
923
- * console.error('Error during shutdown:', error);
924
- * }
925
- * process.exit(0);
926
- * });
927
- * ```
528
+ * After destruction, the container cannot be used.
529
+ * Finalizers run concurrently, so there are no ordering guarantees.
530
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
531
+ * dependencies are cleaned up.
928
532
  *
929
- * @example Handling cleanup errors
930
- * ```typescript
931
- * try {
932
- * await container.destroy();
933
- * } catch (error) {
934
- * if (error instanceof DependencyContainerFinalizationError) {
935
- * console.error('Some dependencies failed to clean up:', error.detail.errors);
936
- * }
937
- * }
938
- * // Container is destroyed regardless of finalizer errors
939
- * ```
533
+ * @throws {DependencyFinalizationError} If any finalizers fail
940
534
  */
941
535
  async destroy() {
942
536
  if (this.isDestroyed) return;
@@ -946,167 +540,153 @@ var Container = class Container {
946
540
  return finalizer(dep);
947
541
  });
948
542
  const results = await Promise.allSettled(promises);
949
- const failures = results.filter((result) => result.status === "rejected");
950
- if (failures.length > 0) throw new DependencyFinalizationError(failures.map((result) => result.reason));
543
+ const failures = results.filter((r) => r.status === "rejected");
544
+ if (failures.length > 0) throw new DependencyFinalizationError(failures.map((r) => r.reason));
951
545
  } finally {
952
546
  this.isDestroyed = true;
953
547
  this.cache.clear();
954
548
  }
955
549
  }
956
550
  };
957
-
958
- //#endregion
959
- //#region src/dependency.ts
960
551
  /**
961
- * Creates a layer that provides a single dependency with inferred requirements.
552
+ * Builder for constructing scoped containers.
962
553
  *
963
- * This is a simplified alternative to `layer()` for the common case of defining
964
- * a single dependency. Unlike `service()` and `autoService()`, this works with
965
- * any tag type (ServiceTag or ValueTag) and doesn't require extending `Tag.Service()`.
966
- *
967
- * Requirements are passed as an optional array of tags, allowing TypeScript to infer
968
- * both the tag type and the requirements automatically - no explicit type
969
- * parameters needed.
970
- *
971
- * @param tag - The tag (ServiceTag or ValueTag) that identifies this dependency
972
- * @param spec - Factory function or lifecycle object for creating the dependency
973
- * @param requirements - Optional array of dependency tags this dependency requires (defaults to [])
974
- * @returns A layer that requires the specified dependencies and provides the tag
975
- *
976
- * @example Simple dependency without requirements
977
- * ```typescript
978
- * const Config = Tag.of('Config')<{ apiUrl: string }>();
979
- *
980
- * // No requirements - can omit the array
981
- * const configDep = dependency(Config, () => ({
982
- * apiUrl: process.env.API_URL!
983
- * }));
984
- * ```
554
+ * @template TTags - Union type of registered dependency tags
555
+ */
556
+ var ScopedContainerBuilder = class {
557
+ factories = new Map();
558
+ finalizers = new Map();
559
+ constructor(scope, parent) {
560
+ this.scope = scope;
561
+ this.parent = parent;
562
+ }
563
+ /**
564
+ * Registers a dependency with a factory function or lifecycle object.
565
+ */
566
+ add(tag, spec) {
567
+ if (typeof spec === "function") {
568
+ this.factories.set(tag, spec);
569
+ this.finalizers.delete(tag);
570
+ } else {
571
+ this.factories.set(tag, spec.create.bind(spec));
572
+ if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
573
+ else this.finalizers.delete(tag);
574
+ }
575
+ return this;
576
+ }
577
+ /**
578
+ * Creates an immutable scoped container from the registered dependencies.
579
+ */
580
+ build() {
581
+ const child = ScopedContainer._createScopedFromBuilder(this.scope, this.parent, this.factories, this.finalizers);
582
+ if (this.parent instanceof ScopedContainer) this.parent._registerChild(child);
583
+ return child;
584
+ }
585
+ };
586
+ /**
587
+ * Scoped container for hierarchical dependency management.
985
588
  *
986
- * @example Dependency with requirements
987
- * ```typescript
988
- * const database = dependency(
989
- * Database,
990
- * async (ctx) => {
991
- * const config = await ctx.resolve(Config);
992
- * const logger = await ctx.resolve(Logger);
993
- * logger.info('Creating database connection');
994
- * return createDb(config.DATABASE);
995
- * },
996
- * [Config, Logger]
997
- * );
998
- * ```
589
+ * Supports parent/child relationships where children can access parent
590
+ * dependencies but maintain their own cache. Useful for request-scoped
591
+ * dependencies in web applications.
999
592
  *
1000
- * @example Dependency with lifecycle (create + cleanup)
1001
- * ```typescript
1002
- * const database = dependency(
1003
- * Database,
1004
- * {
1005
- * create: async (ctx) => {
1006
- * const config = await ctx.resolve(Config);
1007
- * const logger = await ctx.resolve(Logger);
1008
- * logger.info('Creating database connection');
1009
- * return await createDb(config.DATABASE);
1010
- * },
1011
- * cleanup: async (db) => {
1012
- * await disconnectDb(db);
1013
- * },
1014
- * },
1015
- * [Config, Logger]
1016
- * );
1017
- * ```
593
+ * @template TTags - Union type of registered dependency tags
1018
594
  *
1019
- * @example Comparison with layer()
595
+ * @example
1020
596
  * ```typescript
1021
- * // Using layer() - verbose, requires explicit type parameters
1022
- * const database = layer<typeof Config | typeof Logger, typeof Database>(
1023
- * (container) =>
1024
- * container.register(Database, async (ctx) => {
1025
- * const config = await ctx.resolve(Config);
1026
- * return createDb(config.DATABASE);
1027
- * })
1028
- * );
1029
- *
1030
- * // Using dependency() - cleaner, fully inferred types
1031
- * const database = dependency(
1032
- * Database,
1033
- * async (ctx) => {
1034
- * const config = await ctx.resolve(Config);
1035
- * return createDb(config.DATABASE);
1036
- * },
1037
- * [Config, Logger]
1038
- * );
597
+ * // Application-level container
598
+ * const appContainer = ScopedContainer.builder('app')
599
+ * .add(Database, () => new Database())
600
+ * .build();
601
+ *
602
+ * // Request-level container (inherits from app)
603
+ * const requestContainer = appContainer.child('request')
604
+ * .add(RequestContext, () => new RequestContext())
605
+ * .build();
606
+ *
607
+ * // Can resolve both app and request dependencies
608
+ * const db = await requestContainer.resolve(Database);
609
+ * const ctx = await requestContainer.resolve(RequestContext);
1039
610
  * ```
1040
611
  */
1041
- function dependency(tag, spec, requirements) {
1042
- return layer((container) => {
1043
- return container.register(tag, spec);
1044
- });
1045
- }
1046
-
1047
- //#endregion
1048
- //#region src/scoped-container.ts
1049
612
  var ScopedContainer = class ScopedContainer extends Container {
1050
613
  scope;
1051
614
  parent;
1052
615
  children = [];
1053
- constructor(parent, scope) {
1054
- super();
1055
- this.parent = parent;
616
+ /**
617
+ * @internal
618
+ */
619
+ constructor(scope, parent, factories, finalizers) {
620
+ super(factories, finalizers);
1056
621
  this.scope = scope;
622
+ this.parent = parent;
1057
623
  }
1058
624
  /**
1059
- * Creates a new empty scoped container instance.
1060
- * @param scope - The scope identifier for this container
1061
- * @returns A new empty ScopedContainer instance with no registered dependencies
625
+ * @internal - Used by ScopedContainerBuilder
1062
626
  */
1063
- static empty(scope) {
1064
- return new ScopedContainer(null, scope);
627
+ static _createScopedFromBuilder(scope, parent, factories, finalizers) {
628
+ return new ScopedContainer(scope, parent, factories, finalizers);
1065
629
  }
1066
630
  /**
1067
- * Registers a dependency in the scoped container.
631
+ * Creates a new scoped container builder.
1068
632
  *
1069
- * Overrides the base implementation to return ScopedContainer type
1070
- * for proper method chaining support.
633
+ * @param scope - Identifier for the scope (for debugging)
634
+ *
635
+ * @example
636
+ * ```typescript
637
+ * const container = ScopedContainer.builder('app')
638
+ * .add(Database, () => new Database())
639
+ * .build();
640
+ * ```
1071
641
  */
1072
- register(tag, spec) {
1073
- super.register(tag, spec);
1074
- return this;
642
+ static builder(scope) {
643
+ return new ScopedContainerBuilder(scope, null);
1075
644
  }
1076
645
  /**
1077
- * Checks if a dependency has been registered in this scope or any parent scope.
1078
- *
1079
- * This method checks the current scope first, then walks up the parent chain.
1080
- * Returns true if the dependency has been registered somewhere in the scope hierarchy.
646
+ * Creates an empty scoped container with no dependencies.
1081
647
  */
1082
- has(tag) {
1083
- if (super.has(tag)) return true;
1084
- return this.parent?.has(tag) ?? false;
648
+ static empty(scope) {
649
+ return ScopedContainer.builder(scope).build();
1085
650
  }
1086
651
  /**
1087
- * Checks if a dependency has been instantiated in this scope or any parent scope.
652
+ * Creates a scoped container from a layer.
1088
653
  *
1089
- * This method checks the current scope first, then walks up the parent chain.
1090
- * Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
654
+ * This is a convenience method equivalent to applying a layer to
655
+ * `ScopedContainer.builder()` and building the result.
656
+ *
657
+ * @param scope - Identifier for the scope (for debugging)
658
+ * @param layer - A layer with no requirements (all dependencies satisfied)
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * const dbLayer = Layer.service(Database, []);
663
+ * const container = ScopedContainer.from('app', dbLayer);
664
+ *
665
+ * const db = await container.resolve(Database);
666
+ * ```
1091
667
  */
1092
- exists(tag) {
1093
- if (super.exists(tag)) return true;
1094
- return this.parent?.exists(tag) ?? false;
668
+ static from(scope, layer) {
669
+ const builder = ScopedContainer.builder(scope);
670
+ const resultBuilder = layer.apply(builder);
671
+ return resultBuilder.build();
1095
672
  }
1096
673
  /**
1097
- * Retrieves a dependency instance, resolving from the current scope or parent scopes.
674
+ * Resolves a dependency from this scope or parent scopes, creating it if necessary.
1098
675
  *
1099
- * Resolution strategy:
1100
- * 1. Check cache in current scope
1101
- * 2. Check if factory exists in current scope - if so, create instance here
1102
- * 3. Otherwise, delegate to parent scope
1103
- * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
676
+ * Dependencies are singletons - the same instance is returned on subsequent calls.
677
+ *
678
+ * @param tag - The dependency tag to resolve
679
+ * @returns Promise resolving to the dependency instance
680
+ * @throws {ContainerDestroyedError} If the container has been destroyed
681
+ * @throws {UnknownDependencyError} If any dependency is not registered
682
+ * @throws {CircularDependencyError} If a circular dependency is detected
683
+ * @throws {DependencyCreationError} If any factory function throws an error
1104
684
  */
1105
685
  async resolve(tag) {
1106
686
  return this.resolveInternal(tag, []);
1107
687
  }
1108
688
  /**
1109
- * Internal resolution with delegation logic for scoped containers.
689
+ * Internal resolution with parent delegation.
1110
690
  * @internal
1111
691
  */
1112
692
  resolveInternal(tag, chain) {
@@ -1115,22 +695,76 @@ var ScopedContainer = class ScopedContainer extends Container {
1115
695
  throw new UnknownDependencyError(tag);
1116
696
  }
1117
697
  /**
1118
- * Destroys this scoped container and its children, preserving the container structure for reuse.
698
+ * @internal - Used by ScopedContainerBuilder to register children
699
+ */
700
+ _registerChild(child) {
701
+ this.children.push(new WeakRef(child));
702
+ }
703
+ /**
704
+ * Creates a child container builder that inherits from this container.
1119
705
  *
1120
- * This method ensures proper cleanup order while maintaining reusability:
1121
- * 1. Destroys all child scopes first (they may depend on parent scope dependencies)
1122
- * 2. Then calls finalizers for dependencies created in this scope
1123
- * 3. Clears only instance caches - preserves factories, finalizers, and child structure
706
+ * Use this to create a child scope and add dependencies to it.
707
+ * The child can resolve dependencies from this container.
1124
708
  *
1125
- * Child destruction happens first to ensure dependencies don't get cleaned up
1126
- * before their dependents.
709
+ * @param scope - Identifier for the child scope
710
+ * @returns A new ScopedContainerBuilder for the child scope
711
+ * @throws {ContainerDestroyedError} If the container has been destroyed
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * const requestContainer = appContainer.child('request')
716
+ * .add(RequestContext, () => new RequestContext())
717
+ * .build();
718
+ *
719
+ * await requestContainer.resolve(Database); // From parent
720
+ * await requestContainer.resolve(RequestContext); // From this scope
721
+ * ```
722
+ */
723
+ child(scope) {
724
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
725
+ return new ScopedContainerBuilder(scope, this);
726
+ }
727
+ /**
728
+ * Creates a child container with a layer applied.
729
+ *
730
+ * This is a convenience method combining child() + layer.apply() + build().
731
+ * Use this when you have a layer ready to apply.
732
+ *
733
+ * @param scope - Identifier for the child scope
734
+ * @param layer - Layer to apply to the child (can require parent's tags)
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * const requestContainer = appContainer.childFrom('request',
739
+ * userService
740
+ * .provide(Layer.value(TenantContext, tenantCtx))
741
+ * .provide(Layer.value(RequestId, requestId))
742
+ * );
743
+ *
744
+ * const users = await requestContainer.resolve(UserService);
745
+ * ```
746
+ */
747
+ childFrom(scope, layer) {
748
+ return layer.apply(this.child(scope)).build();
749
+ }
750
+ /**
751
+ * Destroys this container and all child containers.
752
+ *
753
+ * Children are destroyed first to ensure proper cleanup order.
754
+ *
755
+ * After destruction, the container cannot be used.
756
+ * Finalizers run concurrently, so there are no ordering guarantees.
757
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
758
+ * dependencies are cleaned up.
759
+ *
760
+ * @throws {DependencyFinalizationError} If any finalizers fail
1127
761
  */
1128
762
  async destroy() {
1129
763
  if (this.isDestroyed) return;
1130
764
  const allFailures = [];
1131
- const childDestroyPromises = this.children.map((weakRef) => weakRef.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
765
+ const childDestroyPromises = this.children.map((ref) => ref.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
1132
766
  const childResults = await Promise.allSettled(childDestroyPromises);
1133
- const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
767
+ const childFailures = childResults.filter((r) => r.status === "rejected").map((r) => r.reason);
1134
768
  allFailures.push(...childFailures);
1135
769
  try {
1136
770
  await super.destroy();
@@ -1141,185 +775,152 @@ var ScopedContainer = class ScopedContainer extends Container {
1141
775
  }
1142
776
  if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
1143
777
  }
1144
- /**
1145
- * Creates a child scoped container.
1146
- *
1147
- * Child containers inherit access to parent dependencies but maintain
1148
- * their own scope for new registrations and instance caching.
1149
- */
1150
- child(scope) {
1151
- if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
1152
- const child = new ScopedContainer(this, scope);
1153
- this.children.push(new WeakRef(child));
1154
- return child;
1155
- }
1156
778
  };
1157
779
 
1158
780
  //#endregion
1159
- //#region src/service.ts
781
+ //#region src/layer.ts
1160
782
  /**
1161
- * Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
1162
- *
1163
- * For ServiceTag services:
1164
- * - Dependencies are automatically inferred from constructor parameters
1165
- * - The factory function must handle dependency injection by resolving dependencies from the container
1166
- *
1167
- * For ValueTag services:
1168
- * - No constructor dependencies are needed since they don't have constructors
1169
- *
1170
- * @template T - The tag representing the service (ServiceTag or ValueTag)
1171
- * @param tag - The tag (ServiceTag or ValueTag)
1172
- * @param factory - Factory function for service instantiation with container
1173
- * @returns The service layer
1174
- *
1175
- * @example Simple service without dependencies
1176
- * ```typescript
1177
- * class LoggerService extends Tag.Service('LoggerService') {
1178
- * log(message: string) { console.log(message); }
1179
- * }
1180
- *
1181
- * const loggerService = service(LoggerService, () => new LoggerService());
1182
- * ```
1183
- *
1184
- * @example Service with dependencies
1185
- * ```typescript
1186
- * class DatabaseService extends Tag.Service('DatabaseService') {
1187
- * query() { return []; }
1188
- * }
1189
- *
1190
- * class UserService extends Tag.Service('UserService') {
1191
- * constructor(private db: DatabaseService) {
1192
- * super();
1193
- * }
1194
- *
1195
- * getUsers() { return this.db.query(); }
1196
- * }
1197
- *
1198
- * const userService = service(UserService, async (ctx) =>
1199
- * new UserService(await ctx.resolve(DatabaseService))
1200
- * );
1201
- * ```
783
+ * The type ID for the Layer interface.
784
+ */
785
+ const LayerTypeId = Symbol.for("sandly/Layer");
786
+ /**
787
+ * Creates a layer from a builder function.
788
+ * @internal
789
+ */
790
+ function createLayer(applyFn) {
791
+ const layerImpl = {
792
+ apply: applyFn,
793
+ provide(dependency) {
794
+ return createProvidedLayer(dependency, layerImpl);
795
+ },
796
+ provideMerge(dependency) {
797
+ return createComposedLayer(dependency, layerImpl);
798
+ },
799
+ merge(other) {
800
+ return createMergedLayer(layerImpl, other);
801
+ }
802
+ };
803
+ return layerImpl;
804
+ }
805
+ /**
806
+ * Creates a layer that only exposes the target's provisions.
807
+ * @internal
1202
808
  */
1203
- function service(tag, spec) {
1204
- return layer((container) => {
1205
- return container.register(tag, spec);
1206
- });
809
+ function createProvidedLayer(dependency, target) {
810
+ return createComposedLayer(dependency, target);
1207
811
  }
1208
812
  /**
1209
- * Creates a service layer with automatic dependency injection by inferring constructor parameters.
1210
- *
1211
- * This is a convenience function that automatically resolves constructor dependencies and passes
1212
- * both DI-managed dependencies and static values to the service constructor in the correct order.
1213
- * It eliminates the need to manually write factory functions for services with constructor dependencies.
1214
- *
1215
- * @template T - The ServiceTag representing the service class
1216
- * @param tag - The service tag (must be a ServiceTag, not a ValueTag)
1217
- * @param deps - Tuple of constructor parameters in order - mix of dependency tags and static values
1218
- * @param finalizer - Optional cleanup function called when the container is destroyed
1219
- * @returns A service layer that automatically handles dependency injection
813
+ * Creates a composed layer that exposes both layers' provisions.
814
+ * @internal
815
+ */
816
+ function createComposedLayer(dependency, target) {
817
+ return {
818
+ apply: (builder) => {
819
+ const withDep = dependency.apply(builder);
820
+ return target.apply(withDep);
821
+ },
822
+ provide(dep) {
823
+ return createProvidedLayer(dep, this);
824
+ },
825
+ provideMerge(dep) {
826
+ return createComposedLayer(dep, this);
827
+ },
828
+ merge(other) {
829
+ return createMergedLayer(this, other);
830
+ }
831
+ };
832
+ }
833
+ /**
834
+ * Creates a merged layer from two independent layers.
835
+ * @internal
836
+ */
837
+ function createMergedLayer(layer1, layer2) {
838
+ return {
839
+ apply: (builder) => {
840
+ const with1 = layer1.apply(builder);
841
+ return layer2.apply(with1);
842
+ },
843
+ provide(dep) {
844
+ return createProvidedLayer(dep, this);
845
+ },
846
+ provideMerge(dep) {
847
+ return createComposedLayer(dep, this);
848
+ },
849
+ merge(other) {
850
+ return createMergedLayer(this, other);
851
+ }
852
+ };
853
+ }
854
+ /**
855
+ * Consolidated Layer API for creating and composing dependency layers.
1220
856
  *
1221
- * @example Simple service with dependencies
857
+ * @example
1222
858
  * ```typescript
1223
- * class DatabaseService extends Tag.Service('DatabaseService') {
1224
- * constructor(private url: string) {
1225
- * super();
1226
- * }
1227
- * connect() { return `Connected to ${this.url}`; }
859
+ * // Define services
860
+ * class Database {
861
+ * query(sql: string) { return []; }
1228
862
  * }
1229
863
  *
1230
- * class UserService extends Tag.Service('UserService') {
1231
- * constructor(private db: DatabaseService, private timeout: number) {
1232
- * super();
1233
- * }
864
+ * class UserService {
865
+ * constructor(private db: Database) {}
1234
866
  * getUsers() { return this.db.query('SELECT * FROM users'); }
1235
867
  * }
1236
868
  *
1237
- * // Automatically inject DatabaseService and pass static timeout value
1238
- * const userService = autoService(UserService, [DatabaseService, 5000]);
1239
- * ```
869
+ * // Create layers
870
+ * const dbLayer = Layer.service(Database, []);
871
+ * const userLayer = Layer.service(UserService, [Database]);
1240
872
  *
1241
- * @example Mixed dependencies and static values
1242
- * ```typescript
1243
- * class NotificationService extends Tag.Service('NotificationService') {
1244
- * constructor(
1245
- * private logger: LoggerService,
1246
- * private apiKey: string,
1247
- * private retries: number,
1248
- * private cache: CacheService
1249
- * ) {
1250
- * super();
1251
- * }
1252
- * }
873
+ * // Compose and create container
874
+ * const appLayer = userLayer.provide(dbLayer);
875
+ * const container = Container.from(appLayer);
1253
876
  *
1254
- * // Mix of DI tags and static values in constructor order
1255
- * const notificationService = autoService(NotificationService, [
1256
- * LoggerService, // Will be resolved from container
1257
- * 'secret-api-key', // Static string value
1258
- * 3, // Static number value
1259
- * CacheService // Will be resolved from container
1260
- * ]);
1261
- * ```
1262
- *
1263
- * @example Compared to manual service creation
1264
- * ```typescript
1265
- * // Manual approach (more verbose)
1266
- * const userServiceManual = service(UserService, async (ctx) => {
1267
- * const db = await ctx.resolve(DatabaseService);
1268
- * return new UserService(db, 5000);
1269
- * });
1270
- *
1271
- * // Auto approach (concise)
1272
- * const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
1273
- * ```
1274
- *
1275
- * @example With finalizer for cleanup
1276
- * ```typescript
1277
- * class DatabaseService extends Tag.Service('DatabaseService') {
1278
- * constructor(private connectionString: string) {
1279
- * super();
1280
- * }
1281
- *
1282
- * private connection: Connection | null = null;
1283
- *
1284
- * async connect() {
1285
- * this.connection = await createConnection(this.connectionString);
1286
- * }
1287
- *
1288
- * async disconnect() {
1289
- * if (this.connection) {
1290
- * await this.connection.close();
1291
- * this.connection = null;
1292
- * }
1293
- * }
1294
- * }
1295
- *
1296
- * // Service with automatic cleanup
1297
- * const dbService = autoService(
1298
- * DatabaseService,
1299
- * {
1300
- * dependencies: ['postgresql://localhost:5432/mydb'],
1301
- * cleanup: (service) => service.disconnect() // Finalizer for cleanup
1302
- * }
1303
- * );
877
+ * const users = await container.resolve(UserService);
1304
878
  * ```
1305
879
  */
1306
- function autoService(tag, spec) {
1307
- if (Array.isArray(spec)) spec = { dependencies: spec };
1308
- const create = async (ctx) => {
1309
- const diDeps = [];
1310
- for (const dep of spec.dependencies) if (Tag.isTag(dep)) diDeps.push(dep);
1311
- const resolved = await ctx.resolveAll(...diDeps);
1312
- const args = [];
1313
- let resolvedIndex = 0;
1314
- for (const dep of spec.dependencies) if (Tag.isTag(dep)) args.push(resolved[resolvedIndex++]);
1315
- else args.push(dep);
1316
- return new tag(...args);
1317
- };
1318
- return service(tag, {
1319
- create,
1320
- cleanup: spec.cleanup
1321
- });
1322
- }
880
+ const Layer = {
881
+ service(cls, deps, options) {
882
+ return createLayer((builder) => {
883
+ return builder.add(cls, {
884
+ create: async (ctx) => {
885
+ const args = await Promise.all(deps.map((dep) => Tag.isTag(dep) ? ctx.resolve(dep) : dep));
886
+ return new cls(...args);
887
+ },
888
+ cleanup: options?.cleanup
889
+ });
890
+ });
891
+ },
892
+ value(tag, value) {
893
+ return createLayer((builder) => {
894
+ return builder.add(tag, () => value);
895
+ });
896
+ },
897
+ create(options) {
898
+ const layer = {
899
+ apply: (builder) => {
900
+ return options.apply(builder);
901
+ },
902
+ provide(dep) {
903
+ return createProvidedLayer(dep, layer);
904
+ },
905
+ provideMerge(dep) {
906
+ return createComposedLayer(dep, layer);
907
+ },
908
+ merge(other) {
909
+ return createMergedLayer(layer, other);
910
+ }
911
+ };
912
+ return layer;
913
+ },
914
+ empty() {
915
+ return createLayer((builder) => builder);
916
+ },
917
+ mergeAll(...layers) {
918
+ return layers.reduce((acc, layer) => acc.merge(layer));
919
+ },
920
+ merge(layer1, layer2) {
921
+ return layer1.merge(layer2);
922
+ }
923
+ };
1323
924
 
1324
925
  //#endregion
1325
- export { CircularDependencyError, Container, ContainerDestroyedError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, InjectSource, Layer, SandlyError, ScopedContainer, Tag, UnknownDependencyError, autoService, constant, dependency, layer, service };
926
+ export { CircularDependencyError, Container, ContainerBuilder, ContainerDestroyedError, DependencyCreationError, DependencyFinalizationError, Layer, SandlyError, ScopedContainer, ScopedContainerBuilder, Tag, UnknownDependencyError };