sandly 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +817 -403
  2. package/dist/index.js +426 -328
  3. package/package.json +75 -76
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
3
  //#region src/tag.ts
4
+ Symbol("InjectSource");
4
5
  /**
5
6
  * Internal symbol used to identify tagged types within the dependency injection system.
6
7
  * This symbol is used as a property key to attach metadata to both value tags and class tags.
@@ -31,8 +32,6 @@ const Tag = {
31
32
  class Tagged {
32
33
  static [TagId] = id;
33
34
  [TagId] = id;
34
- /** @internal */
35
- __type;
36
35
  }
37
36
  return Tagged;
38
37
  },
@@ -89,14 +88,30 @@ var BaseError = class BaseError extends Error {
89
88
  * try {
90
89
  * await container.get(SomeService);
91
90
  * } catch (error) {
92
- * if (error instanceof DependencyContainerError) {
91
+ * if (error instanceof ContainerError) {
93
92
  * console.error('DI Error:', error.message);
94
93
  * console.error('Details:', error.detail);
95
94
  * }
96
95
  * }
97
96
  * ```
98
97
  */
99
- var DependencyContainerError = class extends BaseError {};
98
+ var ContainerError = class extends BaseError {};
99
+ /**
100
+ * Error thrown when attempting to register a dependency that has already been instantiated.
101
+ *
102
+ * This error occurs when calling `container.register()` for a tag that has already been instantiated.
103
+ * Registration must happen before any instantiation occurs, as cached instances would still be used
104
+ * by existing dependencies.
105
+ */
106
+ var DependencyAlreadyInstantiatedError = class extends ContainerError {};
107
+ /**
108
+ * Error thrown when attempting to use a container that has been destroyed.
109
+ *
110
+ * This error occurs when calling `container.get()`, `container.register()`, or `container.destroy()`
111
+ * on a container that has already been destroyed. It indicates a programming error where the container
112
+ * is being used after it has been destroyed.
113
+ */
114
+ var ContainerDestroyedError = class extends ContainerError {};
100
115
  /**
101
116
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
102
117
  *
@@ -117,7 +132,7 @@ var DependencyContainerError = class extends BaseError {};
117
132
  * }
118
133
  * ```
119
134
  */
120
- var UnknownDependencyError = class extends DependencyContainerError {
135
+ var UnknownDependencyError = class extends ContainerError {
121
136
  /**
122
137
  * @internal
123
138
  * Creates an UnknownDependencyError for the given tag.
@@ -141,11 +156,11 @@ var UnknownDependencyError = class extends DependencyContainerError {
141
156
  * class ServiceB extends Tag.Class('ServiceB') {}
142
157
  *
143
158
  * const c = container()
144
- * .register(ServiceA, async (container) =>
145
- * new ServiceA(await container.get(ServiceB)) // Depends on B
159
+ * .register(ServiceA, async (ctx) =>
160
+ * new ServiceA(await ctx.get(ServiceB)) // Depends on B
146
161
  * )
147
- * .register(ServiceB, async (container) =>
148
- * new ServiceB(await container.get(ServiceA)) // Depends on A - CIRCULAR!
162
+ * .register(ServiceB, async (ctx) =>
163
+ * new ServiceB(await ctx.get(ServiceA)) // Depends on A - CIRCULAR!
149
164
  * );
150
165
  *
151
166
  * try {
@@ -158,7 +173,7 @@ var UnknownDependencyError = class extends DependencyContainerError {
158
173
  * }
159
174
  * ```
160
175
  */
161
- var CircularDependencyError = class extends DependencyContainerError {
176
+ var CircularDependencyError = class extends ContainerError {
162
177
  /**
163
178
  * @internal
164
179
  * Creates a CircularDependencyError with the dependency chain information.
@@ -198,7 +213,7 @@ var CircularDependencyError = class extends DependencyContainerError {
198
213
  * }
199
214
  * ```
200
215
  */
201
- var DependencyCreationError = class extends DependencyContainerError {
216
+ var DependencyCreationError = class extends ContainerError {
202
217
  /**
203
218
  * @internal
204
219
  * Creates a DependencyCreationError wrapping the original factory error.
@@ -207,7 +222,7 @@ var DependencyCreationError = class extends DependencyContainerError {
207
222
  * @param error - The original error thrown by the factory function
208
223
  */
209
224
  constructor(tag, error) {
210
- super(`Error creating instance of ${Tag.id(tag)}: ${error}`, {
225
+ super(`Error creating instance of ${Tag.id(tag)}`, {
211
226
  cause: error,
212
227
  detail: { tag: Tag.id(tag) }
213
228
  });
@@ -225,17 +240,17 @@ var DependencyCreationError = class extends DependencyContainerError {
225
240
  * try {
226
241
  * await container.destroy();
227
242
  * } catch (error) {
228
- * if (error instanceof DependencyContainerFinalizationError) {
243
+ * if (error instanceof DependencyFinalizationError) {
229
244
  * console.error('Some finalizers failed');
230
245
  * console.error('Error details:', error.detail.errors);
231
246
  * }
232
247
  * }
233
248
  * ```
234
249
  */
235
- var DependencyContainerFinalizationError = class extends DependencyContainerError {
250
+ var DependencyFinalizationError = class extends ContainerError {
236
251
  /**
237
252
  * @internal
238
- * Creates a DependencyContainerFinalizationError aggregating multiple finalizer failures.
253
+ * Creates a DependencyFinalizationError aggregating multiple finalizer failures.
239
254
  *
240
255
  * @param errors - Array of errors thrown by individual finalizers
241
256
  */
@@ -257,46 +272,6 @@ var DependencyContainerFinalizationError = class extends DependencyContainerErro
257
272
  */
258
273
  const resolutionChain = new AsyncLocalStorage();
259
274
  /**
260
- * Shared logic for dependency resolution that handles caching, circular dependency detection,
261
- * and error handling. Used by both BasicDependencyContainer and ScopedDependencyContainer.
262
- * @internal
263
- */
264
- async function resolveDependency(tag, cache, factories, container$1) {
265
- const cached = cache.get(tag);
266
- if (cached !== void 0) return cached;
267
- const currentChain = resolutionChain.getStore() ?? [];
268
- if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
269
- const factory = factories.get(tag);
270
- if (factory === void 0) return Promise.reject(new UnknownDependencyError(tag));
271
- const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
272
- try {
273
- const instance = await factory(container$1);
274
- return instance;
275
- } catch (error) {
276
- if (error instanceof CircularDependencyError) throw error;
277
- throw new DependencyCreationError(tag, error);
278
- }
279
- }).catch((error) => {
280
- cache.delete(tag);
281
- throw error;
282
- });
283
- cache.set(tag, instancePromise);
284
- return instancePromise;
285
- }
286
- /**
287
- * Shared logic for running finalizers and handling cleanup errors.
288
- * @internal
289
- */
290
- async function runFinalizers(finalizers, cache) {
291
- const promises = Array.from(finalizers.entries()).filter(([tag]) => cache.has(tag)).map(async ([tag, finalizer]) => {
292
- const dep = await cache.get(tag);
293
- return finalizer(dep);
294
- });
295
- const results = await Promise.allSettled(promises);
296
- const failures = results.filter((result) => result.status === "rejected");
297
- if (failures.length > 0) throw new DependencyContainerFinalizationError(failures.map((result) => result.reason));
298
- }
299
- /**
300
275
  * A type-safe dependency injection container that manages service instantiation,
301
276
  * caching, and lifecycle management with support for async dependencies and
302
277
  * circular dependency detection.
@@ -309,7 +284,7 @@ async function runFinalizers(finalizers, cache) {
309
284
  *
310
285
  * @example Basic usage with class tags
311
286
  * ```typescript
312
- * import { container, Tag } from 'sandl';
287
+ * import { container, Tag } from 'sandly';
313
288
  *
314
289
  * class DatabaseService extends Tag.Class('DatabaseService') {
315
290
  * query() { return 'data'; }
@@ -322,8 +297,8 @@ async function runFinalizers(finalizers, cache) {
322
297
  *
323
298
  * const c = container()
324
299
  * .register(DatabaseService, () => new DatabaseService())
325
- * .register(UserService, async (container) =>
326
- * new UserService(await container.get(DatabaseService))
300
+ * .register(UserService, async (ctx) =>
301
+ * new UserService(await ctx.get(DatabaseService))
327
302
  * );
328
303
  *
329
304
  * const userService = await c.get(UserService);
@@ -363,7 +338,7 @@ async function runFinalizers(finalizers, cache) {
363
338
  * await c.destroy(); // Calls all finalizers
364
339
  * ```
365
340
  */
366
- var Container = class {
341
+ var Container = class Container {
367
342
  /**
368
343
  * Cache of instantiated dependencies as promises.
369
344
  * Ensures singleton behavior and supports concurrent access.
@@ -381,18 +356,27 @@ var Container = class {
381
356
  */
382
357
  finalizers = /* @__PURE__ */ new Map();
383
358
  /**
359
+ * Flag indicating whether this container has been destroyed.
360
+ * @internal
361
+ */
362
+ isDestroyed = false;
363
+ /**
384
364
  * Registers a dependency in the container with a factory function and optional finalizer.
385
365
  *
386
366
  * The factory function receives the current container instance and must return the
387
367
  * service instance (or a Promise of it). The container tracks the registration at
388
368
  * the type level, ensuring type safety for subsequent `.get()` calls.
389
369
  *
370
+ * If a dependency is already registered, this method will override it unless the
371
+ * dependency has already been instantiated, in which case it will throw an error.
372
+ *
390
373
  * @template T - The dependency tag being registered
391
374
  * @param tag - The dependency tag (class or value tag)
392
375
  * @param factory - Function that creates the service instance, receives container for dependency injection
393
376
  * @param finalizer - Optional cleanup function called when container is destroyed
394
377
  * @returns A new container instance with the dependency registered
395
- * @throws {DependencyContainerError} If the dependency is already registered
378
+ * @throws {ContainerDestroyedError} If the container has been destroyed
379
+ * @throws {Error} If the dependency has already been instantiated
396
380
  *
397
381
  * @example Registering a simple service
398
382
  * ```typescript
@@ -415,14 +399,21 @@ var Container = class {
415
399
  * const c = container()
416
400
  * .register(DatabaseService, () => new DatabaseService())
417
401
  * .register(LoggerService, () => new LoggerService())
418
- * .register(UserService, async (container) =>
402
+ * .register(UserService, async (ctx) =>
419
403
  * new UserService(
420
- * await container.get(DatabaseService),
421
- * await container.get(LoggerService)
404
+ * await ctx.get(DatabaseService),
405
+ * await ctx.get(LoggerService)
422
406
  * )
423
407
  * );
424
408
  * ```
425
409
  *
410
+ * @example Overriding a dependency
411
+ * ```typescript
412
+ * const c = container()
413
+ * .register(DatabaseService, () => new DatabaseService())
414
+ * .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
415
+ * ```
416
+ *
426
417
  * @example Using value tags
427
418
  * ```typescript
428
419
  * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
@@ -451,35 +442,43 @@ var Container = class {
451
442
  * );
452
443
  * ```
453
444
  */
454
- register(tag, factoryOrLifecycle) {
455
- if (this.factories.has(tag)) throw new DependencyContainerError(`Dependency ${Tag.id(tag)} already registered`);
456
- if (typeof factoryOrLifecycle === "function") this.factories.set(tag, factoryOrLifecycle);
457
- else {
458
- this.factories.set(tag, factoryOrLifecycle.factory);
459
- this.finalizers.set(tag, factoryOrLifecycle.finalizer);
445
+ register(tag, spec) {
446
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot register dependencies on a destroyed container");
447
+ if (this.has(tag) && this.exists(tag)) throw new DependencyAlreadyInstantiatedError(`Cannot register dependency ${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.`);
448
+ if (typeof spec === "function") {
449
+ this.factories.set(tag, spec);
450
+ this.finalizers.delete(tag);
451
+ } else {
452
+ this.factories.set(tag, spec.factory);
453
+ this.finalizers.set(tag, spec.finalizer);
460
454
  }
461
455
  return this;
462
456
  }
463
457
  /**
464
- * Checks if a dependency has been instantiated (cached) in the container.
458
+ * Checks if a dependency has been registered in the container.
465
459
  *
466
- * Note: This returns `true` only after the dependency has been created via `.get()`.
467
- * A registered but not-yet-instantiated dependency will return `false`.
460
+ * This returns `true` if the dependency has been registered via `.register()`,
461
+ * regardless of whether it has been instantiated yet.
468
462
  *
469
463
  * @param tag - The dependency tag to check
470
- * @returns `true` if the dependency has been instantiated and cached, `false` otherwise
464
+ * @returns `true` if the dependency has been registered, `false` otherwise
471
465
  *
472
466
  * @example
473
467
  * ```typescript
474
468
  * const c = container().register(DatabaseService, () => new DatabaseService());
475
- *
476
- * console.log(c.has(DatabaseService)); // false - not instantiated yet
477
- *
478
- * await c.get(DatabaseService);
479
- * console.log(c.has(DatabaseService)); // true - now instantiated and cached
469
+ * console.log(c.has(DatabaseService)); // true
480
470
  * ```
481
471
  */
482
472
  has(tag) {
473
+ return this.factories.has(tag);
474
+ }
475
+ /**
476
+ * Checks if a dependency has been instantiated (cached) in the container.
477
+ *
478
+ * @param tag - The dependency tag to check
479
+ * @returns true if the dependency has been instantiated, false otherwise
480
+ */
481
+ exists(tag) {
483
482
  return this.cache.has(tag);
484
483
  }
485
484
  /**
@@ -524,8 +523,8 @@ var Container = class {
524
523
  * ```typescript
525
524
  * const c = container()
526
525
  * .register(DatabaseService, () => new DatabaseService())
527
- * .register(UserService, async (container) => {
528
- * const db = await container.get(DatabaseService);
526
+ * .register(UserService, async (ctx) => {
527
+ * const db = await ctx.get(DatabaseService);
529
528
  * return new UserService(db);
530
529
  * });
531
530
  *
@@ -533,14 +532,84 @@ var Container = class {
533
532
  * ```
534
533
  */
535
534
  async get(tag) {
536
- return resolveDependency(tag, this.cache, this.factories, this);
535
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
536
+ const cached = this.cache.get(tag);
537
+ if (cached !== void 0) return cached;
538
+ const currentChain = resolutionChain.getStore() ?? [];
539
+ if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
540
+ const factory = this.factories.get(tag);
541
+ if (factory === void 0) throw new UnknownDependencyError(tag);
542
+ const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
543
+ try {
544
+ const instance = await factory(this);
545
+ return instance;
546
+ } catch (error) {
547
+ throw new DependencyCreationError(tag, error);
548
+ }
549
+ }).catch((error) => {
550
+ this.cache.delete(tag);
551
+ throw error;
552
+ });
553
+ this.cache.set(tag, instancePromise);
554
+ return instancePromise;
555
+ }
556
+ /**
557
+ * Copies all registrations from this container to a target container.
558
+ *
559
+ * @internal
560
+ * @param target - The container to copy registrations to
561
+ * @throws {ContainerDestroyedError} If this container has been destroyed
562
+ */
563
+ copyTo(target) {
564
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot copy registrations from a destroyed container");
565
+ for (const [tag, factory] of this.factories) {
566
+ const finalizer = this.finalizers.get(tag);
567
+ if (finalizer) target.register(tag, {
568
+ factory,
569
+ finalizer
570
+ });
571
+ else target.register(tag, factory);
572
+ }
537
573
  }
538
574
  /**
539
- * Destroys all instantiated dependencies by calling their finalizers, then clears the instance cache.
575
+ * Creates a new container by merging this container's registrations with another container.
540
576
  *
541
- * **Important: This method preserves the container structure (factories and finalizers) for reuse.**
542
- * The container can be used again after destruction to create fresh instances following the same
543
- * dependency patterns.
577
+ * This method creates a new container that contains all registrations from both containers.
578
+ * If there are conflicts (same dependency registered in both containers), this
579
+ * container's registration will take precedence.
580
+ *
581
+ * **Important**: Only the registrations are copied, not any cached instances.
582
+ * The new container starts with an empty instance cache.
583
+ *
584
+ * @param other - The container to merge with
585
+ * @returns A new container with combined registrations
586
+ * @throws {ContainerDestroyedError} If this container has been destroyed
587
+ *
588
+ * @example Merging containers
589
+ * ```typescript
590
+ * const container1 = container()
591
+ * .register(DatabaseService, () => new DatabaseService());
592
+ *
593
+ * const container2 = container()
594
+ * .register(UserService, () => new UserService());
595
+ *
596
+ * const merged = container1.merge(container2);
597
+ * // merged has both DatabaseService and UserService
598
+ * ```
599
+ */
600
+ merge(other) {
601
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
602
+ const merged = new Container();
603
+ other.copyTo(merged);
604
+ this.copyTo(merged);
605
+ return merged;
606
+ }
607
+ /**
608
+ * Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
609
+ *
610
+ * **Important: After calling destroy(), the container becomes permanently unusable.**
611
+ * Any subsequent calls to register(), get(), or destroy() will throw a ContainerError.
612
+ * This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
544
613
  *
545
614
  * All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
546
615
  * for maximum cleanup performance.
@@ -552,9 +621,9 @@ var Container = class {
552
621
  * dependencies are cleaned up.
553
622
  *
554
623
  * @returns Promise that resolves when all cleanup is complete
555
- * @throws {DependencyContainerFinalizationError} If any finalizers fail during cleanup
624
+ * @throws {DependencyFinalizationError} If any finalizers fail during cleanup
556
625
  *
557
- * @example Basic cleanup and reuse
626
+ * @example Basic cleanup
558
627
  * ```typescript
559
628
  * const c = container()
560
629
  * .register(DatabaseConnection,
@@ -566,24 +635,32 @@ var Container = class {
566
635
  * (conn) => conn.disconnect() // Finalizer
567
636
  * );
568
637
  *
569
- * // First use cycle
570
- * const db1 = await c.get(DatabaseConnection);
571
- * await c.destroy(); // Calls conn.disconnect(), clears cache
638
+ * const db = await c.get(DatabaseConnection);
639
+ * await c.destroy(); // Calls conn.disconnect(), container becomes unusable
572
640
  *
573
- * // Container can be reused - creates fresh instances
574
- * const db2 = await c.get(DatabaseConnection); // New connection
575
- * expect(db2).not.toBe(db1); // Different instances
641
+ * // This will throw an error
642
+ * try {
643
+ * await c.get(DatabaseConnection);
644
+ * } catch (error) {
645
+ * console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
646
+ * }
576
647
  * ```
577
648
  *
578
- * @example Multiple destroy/reuse cycles
649
+ * @example Application shutdown
579
650
  * ```typescript
580
- * const c = container().register(UserService, () => new UserService());
651
+ * const appContainer = container()
652
+ * .register(DatabaseService, () => new DatabaseService())
653
+ * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.get(DatabaseService)));
581
654
  *
582
- * for (let i = 0; i < 5; i++) {
583
- * const user = await c.get(UserService);
584
- * // ... use service ...
585
- * await c.destroy(); // Clean up, ready for next cycle
586
- * }
655
+ * // During application shutdown
656
+ * process.on('SIGTERM', async () => {
657
+ * try {
658
+ * await appContainer.destroy(); // Clean shutdown of all services
659
+ * } catch (error) {
660
+ * console.error('Error during shutdown:', error);
661
+ * }
662
+ * process.exit(0);
663
+ * });
587
664
  * ```
588
665
  *
589
666
  * @example Handling cleanup errors
@@ -595,143 +672,25 @@ var Container = class {
595
672
  * console.error('Some dependencies failed to clean up:', error.detail.errors);
596
673
  * }
597
674
  * }
598
- * // Container is still reusable even after finalizer errors
675
+ * // Container is destroyed regardless of finalizer errors
599
676
  * ```
600
677
  */
601
678
  async destroy() {
679
+ if (this.isDestroyed) return;
602
680
  try {
603
- await runFinalizers(this.finalizers, this.cache);
681
+ const promises = Array.from(this.finalizers.entries()).filter(([tag]) => this.cache.has(tag)).map(async ([tag, finalizer]) => {
682
+ const dep = await this.cache.get(tag);
683
+ return finalizer(dep);
684
+ });
685
+ const results = await Promise.allSettled(promises);
686
+ const failures = results.filter((result) => result.status === "rejected");
687
+ if (failures.length > 0) throw new DependencyFinalizationError(failures.map((result) => result.reason));
604
688
  } finally {
689
+ this.isDestroyed = true;
605
690
  this.cache.clear();
606
691
  }
607
692
  }
608
693
  };
609
- var ScopedContainer = class ScopedContainer {
610
- scope;
611
- parent;
612
- children = [];
613
- /**
614
- * Cache of instantiated dependencies as promises for this scope.
615
- * @internal
616
- */
617
- cache = /* @__PURE__ */ new Map();
618
- /**
619
- * Factory functions for creating dependency instances in this scope.
620
- * @internal
621
- */
622
- factories = /* @__PURE__ */ new Map();
623
- /**
624
- * Finalizer functions for cleaning up dependencies when this scope is destroyed.
625
- * @internal
626
- */
627
- finalizers = /* @__PURE__ */ new Map();
628
- constructor(parent, scope) {
629
- this.parent = parent;
630
- this.scope = scope;
631
- }
632
- /**
633
- * Registers a dependency in the specified scope within this container's scope chain.
634
- *
635
- * If no scope is specified, registers in the current (leaf) scope. If a scope is specified,
636
- * delegates to the parent container if the target scope doesn't match the current scope.
637
- *
638
- * This allows registering dependencies at different scope levels from any container
639
- * in the scope chain, providing flexibility for dependency organization.
640
- *
641
- * @param tag - The dependency tag to register
642
- * @param factory - Factory function to create the dependency
643
- * @param finalizer - Optional cleanup function
644
- * @param scope - Target scope for registration (defaults to current scope)
645
- * @returns This container with updated type information
646
- *
647
- * @example Registering in different scopes
648
- * ```typescript
649
- * const runtime = scopedContainer('runtime');
650
- * const request = runtime.child('request');
651
- *
652
- * // Register in current (request) scope
653
- * request.register(RequestService, () => new RequestService());
654
- *
655
- * // Register in runtime scope from request container - delegates to parent
656
- * request.register(DatabaseService, () => new DatabaseService(), undefined, 'runtime');
657
- * ```
658
- */
659
- register(tag, factoryOrLifecycle, scope) {
660
- if (scope === void 0 || scope === this.scope) {
661
- if (this.factories.has(tag)) throw new DependencyContainerError(`Dependency ${Tag.id(tag)} already registered in scope '${String(this.scope)}'`);
662
- if (typeof factoryOrLifecycle === "function") this.factories.set(tag, factoryOrLifecycle);
663
- else {
664
- this.factories.set(tag, factoryOrLifecycle.factory);
665
- this.finalizers.set(tag, factoryOrLifecycle.finalizer);
666
- }
667
- return this;
668
- }
669
- if (this.parent === null) throw new DependencyContainerError(`Scope '${String(scope)}' not found in container chain`);
670
- this.parent.register(tag, factoryOrLifecycle, scope);
671
- return this;
672
- }
673
- /**
674
- * Checks if a dependency has been instantiated in this scope or any parent scope.
675
- *
676
- * This method checks the current scope first, then walks up the parent chain.
677
- * Returns true only if the dependency has been created and cached somewhere in the scope hierarchy.
678
- */
679
- has(tag) {
680
- if (this.cache.has(tag)) return true;
681
- return this.parent?.has(tag) ?? false;
682
- }
683
- /**
684
- * Retrieves a dependency instance, resolving from the current scope or parent scopes.
685
- *
686
- * Resolution strategy:
687
- * 1. Check cache in current scope
688
- * 2. Check if factory exists in current scope - if so, create instance here
689
- * 3. Otherwise, delegate to parent scope
690
- * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
691
- */
692
- async get(tag) {
693
- if (this.factories.has(tag)) return resolveDependency(tag, this.cache, this.factories, this);
694
- if (this.parent !== null) return this.parent.get(tag);
695
- throw new UnknownDependencyError(tag);
696
- }
697
- /**
698
- * Destroys this scoped container and its children, preserving the container structure for reuse.
699
- *
700
- * This method ensures proper cleanup order while maintaining reusability:
701
- * 1. Destroys all child scopes first (they may depend on parent scope dependencies)
702
- * 2. Then calls finalizers for dependencies created in this scope
703
- * 3. Clears only instance caches - preserves factories, finalizers, and child structure
704
- *
705
- * Child destruction happens first to ensure dependencies don't get cleaned up
706
- * before their dependents.
707
- */
708
- async destroy() {
709
- const allFailures = [];
710
- try {
711
- const childDestroyPromises = this.children.map((child) => child.destroy());
712
- const childResults = await Promise.allSettled(childDestroyPromises);
713
- const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
714
- allFailures.push(...childFailures);
715
- await runFinalizers(this.finalizers, this.cache);
716
- } catch (error) {
717
- allFailures.push(error);
718
- } finally {
719
- this.cache.clear();
720
- }
721
- if (allFailures.length > 0) throw new DependencyContainerFinalizationError(allFailures);
722
- }
723
- /**
724
- * Creates a child scoped container.
725
- *
726
- * Child containers inherit access to parent dependencies but maintain
727
- * their own scope for new registrations and instance caching.
728
- */
729
- child(scope) {
730
- const child = new ScopedContainer(this, scope);
731
- this.children.push(child);
732
- return child;
733
- }
734
- };
735
694
  /**
736
695
  * Creates a new empty dependency injection container.
737
696
  *
@@ -743,15 +702,15 @@ var ScopedContainer = class ScopedContainer {
743
702
  *
744
703
  * @example
745
704
  * ```typescript
746
- * import { container, Tag } from 'sandl';
705
+ * import { container, Tag } from 'sandly';
747
706
  *
748
707
  * class DatabaseService extends Tag.Class('DatabaseService') {}
749
708
  * class UserService extends Tag.Class('UserService') {}
750
709
  *
751
710
  * const c = container()
752
711
  * .register(DatabaseService, () => new DatabaseService())
753
- * .register(UserService, async (container) =>
754
- * new UserService(await container.get(DatabaseService))
712
+ * .register(UserService, async (ctx) =>
713
+ * new UserService(await ctx.get(DatabaseService))
755
714
  * );
756
715
  *
757
716
  * const userService = await c.get(UserService);
@@ -760,9 +719,6 @@ var ScopedContainer = class ScopedContainer {
760
719
  function container() {
761
720
  return new Container();
762
721
  }
763
- function scopedContainer(scope) {
764
- return new ScopedContainer(null, scope);
765
- }
766
722
 
767
723
  //#endregion
768
724
  //#region src/layer.ts
@@ -772,14 +728,13 @@ function scopedContainer(scope) {
772
728
  *
773
729
  * @template TRequires - The union of dependency tags this layer requires from other layers or external setup
774
730
  * @template TProvides - The union of dependency tags this layer registers/provides
775
- * @template TParams - Optional parameters that can be passed to configure the layer
776
731
  *
777
- * @param register - Function that performs the dependency registrations. Receives a container and optional params.
778
- * @returns A layer factory function. If TParams is undefined, returns a parameterless function. Otherwise returns a function that takes TParams.
732
+ * @param register - Function that performs the dependency registrations. Receives a container.
733
+ * @returns The layer instance.
779
734
  *
780
- * @example Simple layer without parameters
735
+ * @example Simple layer
781
736
  * ```typescript
782
- * import { layer, Tag } from '@/di/layer.js';
737
+ * import { layer, Tag } from 'sandly';
783
738
  *
784
739
  * class DatabaseService extends Tag.Class('DatabaseService') {
785
740
  * constructor(private url: string = 'sqlite://memory') {}
@@ -792,37 +747,7 @@ function scopedContainer(scope) {
792
747
  * );
793
748
  *
794
749
  * // Usage
795
- * const dbLayerInstance = databaseLayer(); // No parameters needed
796
- * ```
797
- *
798
- * @example Layer with dependencies
799
- * ```typescript
800
- * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
801
- *
802
- * // Layer that requires ConfigTag and provides DatabaseService
803
- * const databaseLayer = layer<typeof ConfigTag, typeof DatabaseService>((container) =>
804
- * container.register(DatabaseService, async (c) => {
805
- * const config = await c.get(ConfigTag);
806
- * return new DatabaseService(config.dbUrl);
807
- * })
808
- * );
809
- * ```
810
- *
811
- * @example Parameterized layer
812
- * ```typescript
813
- * interface DatabaseConfig {
814
- * host: string;
815
- * port: number;
816
- * }
817
- *
818
- * // Layer that takes configuration parameters
819
- * const databaseLayer = layer<never, typeof DatabaseService, DatabaseConfig>(
820
- * (container, config) =>
821
- * container.register(DatabaseService, () => new DatabaseService(config))
822
- * );
823
- *
824
- * // Usage with parameters
825
- * const dbLayerInstance = databaseLayer({ host: 'localhost', port: 5432 });
750
+ * const dbLayerInstance = databaseLayer;
826
751
  * ```
827
752
  *
828
753
  * @example Complex application layer structure
@@ -836,73 +761,247 @@ function scopedContainer(scope) {
836
761
  * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
837
762
  * (container) =>
838
763
  * container
839
- * .register(DatabaseService, async (c) => new DatabaseService(await c.get(ConfigTag)))
840
- * .register(CacheService, async (c) => new CacheService(await c.get(ConfigTag)))
764
+ * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.get(ConfigTag)))
765
+ * .register(CacheService, async (ctx) => new CacheService(await ctx.get(ConfigTag)))
841
766
  * );
842
767
  *
843
768
  * // Service layer (requires infrastructure)
844
769
  * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
845
770
  * (container) =>
846
- * container.register(UserService, async (c) =>
847
- * new UserService(await c.get(DatabaseService), await c.get(CacheService))
771
+ * container.register(UserService, async (ctx) =>
772
+ * new UserService(await ctx.get(DatabaseService), await ctx.get(CacheService))
848
773
  * )
849
774
  * );
850
775
  *
851
776
  * // Compose the complete application
852
- * const appLayer = configLayer().to(infraLayer()).to(serviceLayer());
777
+ * const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
853
778
  * ```
854
779
  */
855
780
  function layer(register) {
856
- const factory = (params) => {
857
- const layerImpl = {
858
- register: (container$1) => register(container$1, params),
859
- to(target) {
860
- return createComposedLayer(layerImpl, target);
861
- },
862
- and(other) {
863
- return createMergedLayer(layerImpl, other);
864
- }
865
- };
866
- return layerImpl;
781
+ const layerImpl = {
782
+ register: (container$1) => register(container$1),
783
+ provide(dependency) {
784
+ return createProvidedLayer(dependency, layerImpl);
785
+ },
786
+ provideMerge(dependency) {
787
+ return createComposedLayer(dependency, layerImpl);
788
+ },
789
+ merge(other) {
790
+ return createMergedLayer(layerImpl, other);
791
+ }
867
792
  };
868
- return factory;
793
+ return layerImpl;
794
+ }
795
+ /**
796
+ * Internal function to create a provided layer from two layers.
797
+ * This implements the `.provide()` method logic - only exposes target layer's provisions.
798
+ *
799
+ * @internal
800
+ */
801
+ function createProvidedLayer(dependency, target) {
802
+ return createComposedLayer(dependency, target);
869
803
  }
870
804
  /**
871
805
  * Internal function to create a composed layer from two layers.
872
- * This implements the `.to()` method logic.
806
+ * This implements the `.provideMerge()` method logic - exposes both layers' provisions.
873
807
  *
874
808
  * @internal
875
809
  */
876
- function createComposedLayer(source, target) {
810
+ function createComposedLayer(dependency, target) {
877
811
  return layer((container$1) => {
878
- const containerWithSource = source.register(container$1);
879
- return target.register(containerWithSource);
880
- })();
812
+ const containerWithDependency = dependency.register(container$1);
813
+ return target.register(containerWithDependency);
814
+ });
881
815
  }
882
816
  /**
883
817
  * Internal function to create a merged layer from two layers.
884
- * This implements the `.and()` method logic.
818
+ * This implements the `.merge()` method logic.
885
819
  *
886
820
  * @internal
887
821
  */
888
822
  function createMergedLayer(layer1, layer2) {
889
823
  return layer((container$1) => {
890
824
  const container1 = layer1.register(container$1);
891
- return layer2.register(container1);
892
- })();
825
+ const container2 = layer2.register(container1);
826
+ return container2;
827
+ });
893
828
  }
894
829
  /**
895
830
  * Utility object containing helper functions for working with layers.
896
831
  */
897
832
  const Layer = {
898
833
  empty() {
899
- return layer((container$1) => container$1)();
834
+ return layer((container$1) => container$1);
900
835
  },
901
- merge(...layers) {
902
- return layers.reduce((acc, layer$1) => acc.and(layer$1));
836
+ mergeAll(...layers) {
837
+ return layers.reduce((acc, layer$1) => acc.merge(layer$1));
838
+ },
839
+ merge(layer1, layer2) {
840
+ return layer1.merge(layer2);
903
841
  }
904
842
  };
905
843
 
844
+ //#endregion
845
+ //#region src/scoped-container.ts
846
+ var ScopedContainer = class ScopedContainer extends Container {
847
+ scope;
848
+ parent;
849
+ children = [];
850
+ constructor(parent, scope) {
851
+ super();
852
+ this.parent = parent;
853
+ this.scope = scope;
854
+ }
855
+ /**
856
+ * Registers a dependency in the scoped container.
857
+ *
858
+ * Overrides the base implementation to return ScopedContainer type
859
+ * for proper method chaining support.
860
+ */
861
+ register(tag, spec) {
862
+ super.register(tag, spec);
863
+ return this;
864
+ }
865
+ /**
866
+ * Checks if a dependency has been registered in this scope or any parent scope.
867
+ *
868
+ * This method checks the current scope first, then walks up the parent chain.
869
+ * Returns true if the dependency has been registered somewhere in the scope hierarchy.
870
+ */
871
+ has(tag) {
872
+ if (super.has(tag)) return true;
873
+ return this.parent?.has(tag) ?? false;
874
+ }
875
+ /**
876
+ * Checks if a dependency has been instantiated in this scope or any parent scope.
877
+ *
878
+ * This method checks the current scope first, then walks up the parent chain.
879
+ * Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
880
+ */
881
+ exists(tag) {
882
+ if (super.exists(tag)) return true;
883
+ return this.parent?.exists(tag) ?? false;
884
+ }
885
+ /**
886
+ * Retrieves a dependency instance, resolving from the current scope or parent scopes.
887
+ *
888
+ * Resolution strategy:
889
+ * 1. Check cache in current scope
890
+ * 2. Check if factory exists in current scope - if so, create instance here
891
+ * 3. Otherwise, delegate to parent scope
892
+ * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
893
+ */
894
+ async get(tag) {
895
+ if (this.factories.has(tag)) return super.get(tag);
896
+ if (this.parent !== null) return this.parent.get(tag);
897
+ throw new UnknownDependencyError(tag);
898
+ }
899
+ /**
900
+ * Destroys this scoped container and its children, preserving the container structure for reuse.
901
+ *
902
+ * This method ensures proper cleanup order while maintaining reusability:
903
+ * 1. Destroys all child scopes first (they may depend on parent scope dependencies)
904
+ * 2. Then calls finalizers for dependencies created in this scope
905
+ * 3. Clears only instance caches - preserves factories, finalizers, and child structure
906
+ *
907
+ * Child destruction happens first to ensure dependencies don't get cleaned up
908
+ * before their dependents.
909
+ */
910
+ async destroy() {
911
+ if (this.isDestroyed) return;
912
+ const allFailures = [];
913
+ const childDestroyPromises = this.children.map((weakRef) => weakRef.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
914
+ const childResults = await Promise.allSettled(childDestroyPromises);
915
+ const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
916
+ allFailures.push(...childFailures);
917
+ try {
918
+ await super.destroy();
919
+ } catch (error) {
920
+ allFailures.push(error);
921
+ } finally {
922
+ this.parent = null;
923
+ }
924
+ if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
925
+ }
926
+ /**
927
+ * Creates a new scoped container by merging this container's registrations with another container.
928
+ *
929
+ * This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
930
+ * The resulting scoped container contains all registrations from both containers and becomes a root scope
931
+ * (no parent) with the scope name from this container.
932
+ *
933
+ * @param other - The container to merge with
934
+ * @returns A new ScopedContainer with combined registrations
935
+ * @throws {ContainerDestroyedError} If this container has been destroyed
936
+ */
937
+ merge(other) {
938
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
939
+ const merged = new ScopedContainer(null, this.scope);
940
+ other.copyTo(merged);
941
+ this.copyTo(merged);
942
+ return merged;
943
+ }
944
+ /**
945
+ * Creates a child scoped container.
946
+ *
947
+ * Child containers inherit access to parent dependencies but maintain
948
+ * their own scope for new registrations and instance caching.
949
+ */
950
+ child(scope) {
951
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
952
+ const child = new ScopedContainer(this, scope);
953
+ this.children.push(new WeakRef(child));
954
+ return child;
955
+ }
956
+ };
957
+ /**
958
+ * Converts a regular container into a scoped container, copying all registrations.
959
+ *
960
+ * This function creates a new ScopedContainer instance and copies all factory functions
961
+ * and finalizers from the source container. The resulting scoped container becomes a root
962
+ * scope (no parent) with all the same dependency registrations.
963
+ *
964
+ * **Important**: Only the registrations are copied, not any cached instances.
965
+ * The new scoped container starts with an empty instance cache.
966
+ *
967
+ * @param container - The container to convert to a scoped container
968
+ * @param scope - A string or symbol identifier for this scope (used for debugging)
969
+ * @returns A new ScopedContainer instance with all registrations copied from the source container
970
+ * @throws {ContainerDestroyedError} If the source container has been destroyed
971
+ *
972
+ * @example Converting a regular container to scoped
973
+ * ```typescript
974
+ * import { container, scoped } from 'sandly';
975
+ *
976
+ * const appContainer = container()
977
+ * .register(DatabaseService, () => new DatabaseService())
978
+ * .register(ConfigService, () => new ConfigService());
979
+ *
980
+ * const scopedAppContainer = scoped(appContainer, 'app');
981
+ *
982
+ * // Create child scopes
983
+ * const requestContainer = scopedAppContainer.child('request');
984
+ * ```
985
+ *
986
+ * @example Copying complex registrations
987
+ * ```typescript
988
+ * const baseContainer = container()
989
+ * .register(DatabaseService, () => new DatabaseService())
990
+ * .register(UserService, {
991
+ * factory: async (ctx) => new UserService(await ctx.get(DatabaseService)),
992
+ * finalizer: (service) => service.cleanup()
993
+ * });
994
+ *
995
+ * const scopedContainer = scoped(baseContainer, 'app');
996
+ * // scopedContainer now has all the same registrations with finalizers preserved
997
+ * ```
998
+ */
999
+ function scoped(container$1, scope) {
1000
+ const emptyScoped = new ScopedContainer(null, scope);
1001
+ const result = emptyScoped.merge(container$1);
1002
+ return result;
1003
+ }
1004
+
906
1005
  //#endregion
907
1006
  //#region src/service.ts
908
1007
  /**
@@ -916,10 +1015,9 @@ const Layer = {
916
1015
  * - No constructor dependencies are needed since they don't have constructors
917
1016
  *
918
1017
  * @template T - The tag representing the service (ClassTag or ValueTag)
919
- * @template TParams - Optional parameters for service configuration
920
1018
  * @param serviceClass - The tag (ClassTag or ValueTag)
921
- * @param factory - Factory function for service instantiation with container and optional params
922
- * @returns A factory function that creates a service layer
1019
+ * @param factory - Factory function for service instantiation with container
1020
+ * @returns The service layer
923
1021
  *
924
1022
  * @example Simple service without dependencies
925
1023
  * ```typescript
@@ -944,40 +1042,40 @@ const Layer = {
944
1042
  * getUsers() { return this.db.query(); }
945
1043
  * }
946
1044
  *
947
- * const userService = service(UserService, async (container) =>
948
- * new UserService(await container.get(DatabaseService))
1045
+ * const userService = service(UserService, async (ctx) =>
1046
+ * new UserService(await ctx.get(DatabaseService))
949
1047
  * );
950
1048
  * ```
1049
+ */
1050
+ function service(serviceClass, spec) {
1051
+ return layer((container$1) => {
1052
+ return container$1.register(serviceClass, spec);
1053
+ });
1054
+ }
1055
+
1056
+ //#endregion
1057
+ //#region src/value.ts
1058
+ /**
1059
+ * Creates a layer that provides a constant value for a given tag.
951
1060
  *
952
- * @example Service with configuration parameters
1061
+ * @param tag - The value tag to provide
1062
+ * @param constantValue - The constant value to provide
1063
+ * @returns A layer with no dependencies that provides the constant value
1064
+ *
1065
+ * @example
953
1066
  * ```typescript
954
- * class DatabaseService extends Tag.Class('DatabaseService') {
955
- * constructor(private config: { dbUrl: string }) {
956
- * super();
957
- * }
958
- * }
1067
+ * const ApiKey = Tag.of('ApiKey')<string>();
1068
+ * const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
959
1069
  *
960
- * const dbService = service(
961
- * DatabaseService,
962
- * (container, params: { dbUrl: string }) => new DatabaseService(params)
963
- * );
1070
+ * const apiKey = value(ApiKey, 'my-secret-key');
1071
+ * const dbUrl = value(DatabaseUrl, 'postgresql://localhost:5432/myapp');
1072
+ *
1073
+ * const config = Layer.merge(apiKey, dbUrl);
964
1074
  * ```
965
1075
  */
966
- function service(serviceClass, factory) {
967
- const serviceFactory = (params) => {
968
- const serviceLayer = layer((container$1) => {
969
- return container$1.register(serviceClass, (c) => factory(c, params));
970
- })();
971
- const serviceImpl = {
972
- serviceClass,
973
- register: serviceLayer.register,
974
- to: serviceLayer.to,
975
- and: serviceLayer.and
976
- };
977
- return serviceImpl;
978
- };
979
- return serviceFactory;
1076
+ function value(tag, constantValue) {
1077
+ return layer((container$1) => container$1.register(tag, () => constantValue));
980
1078
  }
981
1079
 
982
1080
  //#endregion
983
- export { Layer, Tag, container, layer, scopedContainer, service };
1081
+ export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, Layer, ScopedContainer, Tag, UnknownDependencyError, container, layer, scoped, service, value };