sandly 0.0.2 → 0.2.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 +19 -16
  2. package/dist/index.d.ts +1029 -470
  3. package/dist/index.js +572 -417
  4. package/package.json +75 -76
package/dist/index.js CHANGED
@@ -1,50 +1,77 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
+ //#region src/utils/object.ts
4
+ function hasKey(obj, key) {
5
+ return obj !== void 0 && obj !== null && (typeof obj === "object" || typeof obj === "function") && key in obj;
6
+ }
7
+ function getKey(obj, ...keys) {
8
+ let current = obj;
9
+ for (const key of keys) {
10
+ if (!hasKey(current, key)) return void 0;
11
+ current = current[key];
12
+ }
13
+ return current;
14
+ }
15
+
16
+ //#endregion
3
17
  //#region src/tag.ts
4
18
  /**
5
- * Internal symbol used to identify tagged types within the dependency injection system.
6
- * This symbol is used as a property key to attach metadata to both value tags and class tags.
19
+ * Symbol used to identify tagged types within the dependency injection system.
20
+ * This symbol is used as a property key to attach metadata to both value tags and service tags.
21
+ *
22
+ * Note: We can't use a symbol here becuase it produced the following TS error:
23
+ * error TS4020: 'extends' clause of exported class 'NotificationService' has or is using private name 'TagIdKey'.
24
+ *
25
+ * @internal
26
+ */
27
+ const ValueTagIdKey = "__sandly/ValueTagIdKey__";
28
+ const ServiceTagIdKey = "__sandly/ServiceTagIdKey__";
29
+ /**
30
+ * Internal symbol used to identify the type of a tagged type within the dependency injection system.
31
+ * This symbol is used as a property key to attach metadata to both value tags and service tags.
32
+ * It is used to carry the type of the tagged type and should not be used directly.
7
33
  * @internal
8
34
  */
9
- const TagId = "__tag_id__";
35
+ const TagTypeKey = Symbol.for("sandly/TagTypeKey");
10
36
  /**
11
37
  * Utility object containing factory functions for creating dependency tags.
12
38
  *
13
- * The Tag object provides the primary API for creating both value tags and class tags
39
+ * The Tag object provides the primary API for creating both value tags and service tags
14
40
  * used throughout the dependency injection system. It's the main entry point for
15
41
  * defining dependencies in a type-safe way.
16
42
  */
17
43
  const Tag = {
18
44
  of: (id) => {
19
45
  return () => ({
20
- [TagId]: id,
21
- __type: void 0
46
+ [ValueTagIdKey]: id,
47
+ [TagTypeKey]: void 0
22
48
  });
23
49
  },
24
50
  for: () => {
25
51
  return {
26
- [TagId]: Symbol(),
27
- __type: void 0
52
+ [ValueTagIdKey]: Symbol(),
53
+ [TagTypeKey]: void 0
28
54
  };
29
55
  },
30
- Class: (id) => {
56
+ Service: (id) => {
31
57
  class Tagged {
32
- static [TagId] = id;
33
- [TagId] = id;
34
- /** @internal */
35
- __type;
58
+ static [ServiceTagIdKey] = id;
59
+ [ServiceTagIdKey] = id;
36
60
  }
37
61
  return Tagged;
38
62
  },
39
63
  id: (tag) => {
40
- if (typeof tag === "function") {
41
- const id$1 = tag[TagId];
42
- return typeof id$1 === "symbol" ? id$1.toString() : String(id$1);
43
- }
44
- const id = tag[TagId];
45
- return typeof id === "symbol" ? id.toString() : String(id);
64
+ return typeof tag === "function" ? tag[ServiceTagIdKey] : tag[ValueTagIdKey];
65
+ },
66
+ isTag: (tag) => {
67
+ return typeof tag === "function" ? getKey(tag, ServiceTagIdKey) !== void 0 : getKey(tag, ValueTagIdKey) !== void 0;
46
68
  }
47
69
  };
70
+ /**
71
+ * Unique symbol used to store the original ValueTag in Inject<T> types.
72
+ * This prevents property name collisions while allowing type-level extraction.
73
+ */
74
+ const InjectSource = Symbol("InjectSource");
48
75
 
49
76
  //#endregion
50
77
  //#region src/errors.ts
@@ -87,29 +114,45 @@ var BaseError = class BaseError extends Error {
87
114
  * @example Catching DI errors
88
115
  * ```typescript
89
116
  * try {
90
- * await container.get(SomeService);
117
+ * await container.resolve(SomeService);
91
118
  * } catch (error) {
92
- * if (error instanceof DependencyContainerError) {
119
+ * if (error instanceof ContainerError) {
93
120
  * console.error('DI Error:', error.message);
94
121
  * console.error('Details:', error.detail);
95
122
  * }
96
123
  * }
97
124
  * ```
98
125
  */
99
- var DependencyContainerError = class extends BaseError {};
126
+ var ContainerError = class extends BaseError {};
127
+ /**
128
+ * Error thrown when attempting to register a dependency that has already been instantiated.
129
+ *
130
+ * This error occurs when calling `container.register()` for a tag that has already been instantiated.
131
+ * Registration must happen before any instantiation occurs, as cached instances would still be used
132
+ * by existing dependencies.
133
+ */
134
+ var DependencyAlreadyInstantiatedError = class extends ContainerError {};
135
+ /**
136
+ * Error thrown when attempting to use a container that has been destroyed.
137
+ *
138
+ * This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
139
+ * on a container that has already been destroyed. It indicates a programming error where the container
140
+ * is being used after it has been destroyed.
141
+ */
142
+ var ContainerDestroyedError = class extends ContainerError {};
100
143
  /**
101
144
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
102
145
  *
103
- * This error occurs when calling `container.get(Tag)` for a tag that was never
146
+ * This error occurs when calling `container.resolve(Tag)` for a tag that was never
104
147
  * registered via `container.register()`. It indicates a programming error where
105
148
  * the dependency setup is incomplete.
106
149
  *
107
150
  * @example
108
151
  * ```typescript
109
- * const c = container(); // Empty container
152
+ * const c = Container.empty(); // Empty container
110
153
  *
111
154
  * try {
112
- * await c.get(UnregisteredService); // This will throw
155
+ * await c.resolve(UnregisteredService); // This will throw
113
156
  * } catch (error) {
114
157
  * if (error instanceof UnknownDependencyError) {
115
158
  * console.error('Missing dependency:', error.message);
@@ -117,7 +160,7 @@ var DependencyContainerError = class extends BaseError {};
117
160
  * }
118
161
  * ```
119
162
  */
120
- var UnknownDependencyError = class extends DependencyContainerError {
163
+ var UnknownDependencyError = class extends ContainerError {
121
164
  /**
122
165
  * @internal
123
166
  * Creates an UnknownDependencyError for the given tag.
@@ -125,7 +168,7 @@ var UnknownDependencyError = class extends DependencyContainerError {
125
168
  * @param tag - The dependency tag that wasn't found
126
169
  */
127
170
  constructor(tag) {
128
- super(`No factory registered for dependency ${Tag.id(tag)}`);
171
+ super(`No factory registered for dependency ${String(Tag.id(tag))}`);
129
172
  }
130
173
  };
131
174
  /**
@@ -137,19 +180,19 @@ var UnknownDependencyError = class extends DependencyContainerError {
137
180
  *
138
181
  * @example Circular dependency scenario
139
182
  * ```typescript
140
- * class ServiceA extends Tag.Class('ServiceA') {}
141
- * class ServiceB extends Tag.Class('ServiceB') {}
183
+ * class ServiceA extends Tag.Service('ServiceA') {}
184
+ * class ServiceB extends Tag.Service('ServiceB') {}
142
185
  *
143
- * const c = container()
144
- * .register(ServiceA, async (container) =>
145
- * new ServiceA(await container.get(ServiceB)) // Depends on B
186
+ * const c = Container.empty()
187
+ * .register(ServiceA, async (ctx) =>
188
+ * new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
146
189
  * )
147
- * .register(ServiceB, async (container) =>
148
- * new ServiceB(await container.get(ServiceA)) // Depends on A - CIRCULAR!
190
+ * .register(ServiceB, async (ctx) =>
191
+ * new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
149
192
  * );
150
193
  *
151
194
  * try {
152
- * await c.get(ServiceA);
195
+ * await c.resolve(ServiceA);
153
196
  * } catch (error) {
154
197
  * if (error instanceof CircularDependencyError) {
155
198
  * console.error('Circular dependency:', error.message);
@@ -158,7 +201,7 @@ var UnknownDependencyError = class extends DependencyContainerError {
158
201
  * }
159
202
  * ```
160
203
  */
161
- var CircularDependencyError = class extends DependencyContainerError {
204
+ var CircularDependencyError = class extends ContainerError {
162
205
  /**
163
206
  * @internal
164
207
  * Creates a CircularDependencyError with the dependency chain information.
@@ -168,7 +211,7 @@ var CircularDependencyError = class extends DependencyContainerError {
168
211
  */
169
212
  constructor(tag, dependencyChain) {
170
213
  const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
171
- super(`Circular dependency detected for ${Tag.id(tag)}: ${chain} -> ${Tag.id(tag)}`, { detail: {
214
+ super(`Circular dependency detected for ${String(Tag.id(tag))}: ${chain} -> ${String(Tag.id(tag))}`, { detail: {
172
215
  tag: Tag.id(tag),
173
216
  dependencyChain: dependencyChain.map((t) => Tag.id(t))
174
217
  } });
@@ -182,14 +225,14 @@ var CircularDependencyError = class extends DependencyContainerError {
182
225
  *
183
226
  * @example Factory throwing error
184
227
  * ```typescript
185
- * class DatabaseService extends Tag.Class('DatabaseService') {}
228
+ * class DatabaseService extends Tag.Service('DatabaseService') {}
186
229
  *
187
- * const c = container().register(DatabaseService, () => {
230
+ * const c = Container.empty().register(DatabaseService, () => {
188
231
  * throw new Error('Database connection failed');
189
232
  * });
190
233
  *
191
234
  * try {
192
- * await c.get(DatabaseService);
235
+ * await c.resolve(DatabaseService);
193
236
  * } catch (error) {
194
237
  * if (error instanceof DependencyCreationError) {
195
238
  * console.error('Failed to create:', error.message);
@@ -198,7 +241,7 @@ var CircularDependencyError = class extends DependencyContainerError {
198
241
  * }
199
242
  * ```
200
243
  */
201
- var DependencyCreationError = class extends DependencyContainerError {
244
+ var DependencyCreationError = class extends ContainerError {
202
245
  /**
203
246
  * @internal
204
247
  * Creates a DependencyCreationError wrapping the original factory error.
@@ -207,7 +250,7 @@ var DependencyCreationError = class extends DependencyContainerError {
207
250
  * @param error - The original error thrown by the factory function
208
251
  */
209
252
  constructor(tag, error) {
210
- super(`Error creating instance of ${Tag.id(tag)}: ${error}`, {
253
+ super(`Error creating instance of ${String(Tag.id(tag))}`, {
211
254
  cause: error,
212
255
  detail: { tag: Tag.id(tag) }
213
256
  });
@@ -225,17 +268,17 @@ var DependencyCreationError = class extends DependencyContainerError {
225
268
  * try {
226
269
  * await container.destroy();
227
270
  * } catch (error) {
228
- * if (error instanceof DependencyContainerFinalizationError) {
271
+ * if (error instanceof DependencyFinalizationError) {
229
272
  * console.error('Some finalizers failed');
230
273
  * console.error('Error details:', error.detail.errors);
231
274
  * }
232
275
  * }
233
276
  * ```
234
277
  */
235
- var DependencyContainerFinalizationError = class extends DependencyContainerError {
278
+ var DependencyFinalizationError = class extends ContainerError {
236
279
  /**
237
280
  * @internal
238
- * Creates a DependencyContainerFinalizationError aggregating multiple finalizer failures.
281
+ * Creates a DependencyFinalizationError aggregating multiple finalizer failures.
239
282
  *
240
283
  * @param errors - Array of errors thrown by individual finalizers
241
284
  */
@@ -256,46 +299,7 @@ var DependencyContainerFinalizationError = class extends DependencyContainerErro
256
299
  * @internal
257
300
  */
258
301
  const resolutionChain = new AsyncLocalStorage();
259
- /**
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
- }
302
+ const ContainerTypeId = Symbol.for("sandly/Container");
299
303
  /**
300
304
  * A type-safe dependency injection container that manages service instantiation,
301
305
  * caching, and lifecycle management with support for async dependencies and
@@ -307,26 +311,26 @@ async function runFinalizers(finalizers, cache) {
307
311
  *
308
312
  * @template TReg - Union type of all registered dependency tags in this container
309
313
  *
310
- * @example Basic usage with class tags
314
+ * @example Basic usage with service tags
311
315
  * ```typescript
312
- * import { container, Tag } from 'sandl';
316
+ * import { container, Tag } from 'sandly';
313
317
  *
314
- * class DatabaseService extends Tag.Class('DatabaseService') {
318
+ * class DatabaseService extends Tag.Service('DatabaseService') {
315
319
  * query() { return 'data'; }
316
320
  * }
317
321
  *
318
- * class UserService extends Tag.Class('UserService') {
322
+ * class UserService extends Tag.Service('UserService') {
319
323
  * constructor(private db: DatabaseService) {}
320
324
  * getUser() { return this.db.query(); }
321
325
  * }
322
326
  *
323
- * const c = container()
327
+ * const c = Container.empty()
324
328
  * .register(DatabaseService, () => new DatabaseService())
325
- * .register(UserService, async (container) =>
326
- * new UserService(await container.get(DatabaseService))
329
+ * .register(UserService, async (ctx) =>
330
+ * new UserService(await ctx.resolve(DatabaseService))
327
331
  * );
328
332
  *
329
- * const userService = await c.get(UserService);
333
+ * const userService = await c.resolve(UserService);
330
334
  * ```
331
335
  *
332
336
  * @example Usage with value tags
@@ -334,22 +338,22 @@ async function runFinalizers(finalizers, cache) {
334
338
  * const ApiKeyTag = Tag.of('apiKey')<string>();
335
339
  * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
336
340
  *
337
- * const c = container()
341
+ * const c = Container.empty()
338
342
  * .register(ApiKeyTag, () => process.env.API_KEY!)
339
343
  * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
340
344
  *
341
- * const apiKey = await c.get(ApiKeyTag);
342
- * const config = await c.get(ConfigTag);
345
+ * const apiKey = await c.resolve(ApiKeyTag);
346
+ * const config = await c.resolve(ConfigTag);
343
347
  * ```
344
348
  *
345
349
  * @example With finalizers for cleanup
346
350
  * ```typescript
347
- * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
351
+ * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
348
352
  * async connect() { return; }
349
353
  * async disconnect() { return; }
350
354
  * }
351
355
  *
352
- * const c = container().register(
356
+ * const c = Container.empty().register(
353
357
  * DatabaseConnection,
354
358
  * async () => {
355
359
  * const conn = new DatabaseConnection();
@@ -363,7 +367,8 @@ async function runFinalizers(finalizers, cache) {
363
367
  * await c.destroy(); // Calls all finalizers
364
368
  * ```
365
369
  */
366
- var Container = class {
370
+ var Container = class Container {
371
+ [ContainerTypeId];
367
372
  /**
368
373
  * Cache of instantiated dependencies as promises.
369
374
  * Ensures singleton behavior and supports concurrent access.
@@ -381,26 +386,38 @@ var Container = class {
381
386
  */
382
387
  finalizers = /* @__PURE__ */ new Map();
383
388
  /**
389
+ * Flag indicating whether this container has been destroyed.
390
+ * @internal
391
+ */
392
+ isDestroyed = false;
393
+ static empty() {
394
+ return new Container();
395
+ }
396
+ /**
384
397
  * Registers a dependency in the container with a factory function and optional finalizer.
385
398
  *
386
399
  * The factory function receives the current container instance and must return the
387
400
  * service instance (or a Promise of it). The container tracks the registration at
388
- * the type level, ensuring type safety for subsequent `.get()` calls.
401
+ * the type level, ensuring type safety for subsequent `.resolve()` calls.
402
+ *
403
+ * If a dependency is already registered, this method will override it unless the
404
+ * dependency has already been instantiated, in which case it will throw an error.
389
405
  *
390
406
  * @template T - The dependency tag being registered
391
407
  * @param tag - The dependency tag (class or value tag)
392
408
  * @param factory - Function that creates the service instance, receives container for dependency injection
393
409
  * @param finalizer - Optional cleanup function called when container is destroyed
394
410
  * @returns A new container instance with the dependency registered
395
- * @throws {DependencyContainerError} If the dependency is already registered
411
+ * @throws {ContainerDestroyedError} If the container has been destroyed
412
+ * @throws {Error} If the dependency has already been instantiated
396
413
  *
397
414
  * @example Registering a simple service
398
415
  * ```typescript
399
- * class LoggerService extends Tag.Class('LoggerService') {
416
+ * class LoggerService extends Tag.Service('LoggerService') {
400
417
  * log(message: string) { console.log(message); }
401
418
  * }
402
419
  *
403
- * const c = container().register(
420
+ * const c = Container.empty().register(
404
421
  * LoggerService,
405
422
  * () => new LoggerService()
406
423
  * );
@@ -408,26 +425,33 @@ var Container = class {
408
425
  *
409
426
  * @example Registering with dependencies
410
427
  * ```typescript
411
- * class UserService extends Tag.Class('UserService') {
428
+ * class UserService extends Tag.Service('UserService') {
412
429
  * constructor(private db: DatabaseService, private logger: LoggerService) {}
413
430
  * }
414
431
  *
415
- * const c = container()
432
+ * const c = Container.empty()
416
433
  * .register(DatabaseService, () => new DatabaseService())
417
434
  * .register(LoggerService, () => new LoggerService())
418
- * .register(UserService, async (container) =>
435
+ * .register(UserService, async (ctx) =>
419
436
  * new UserService(
420
- * await container.get(DatabaseService),
421
- * await container.get(LoggerService)
437
+ * await ctx.resolve(DatabaseService),
438
+ * await ctx.resolve(LoggerService)
422
439
  * )
423
440
  * );
424
441
  * ```
425
442
  *
443
+ * @example Overriding a dependency
444
+ * ```typescript
445
+ * const c = Container.empty()
446
+ * .register(DatabaseService, () => new DatabaseService())
447
+ * .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
448
+ * ```
449
+ *
426
450
  * @example Using value tags
427
451
  * ```typescript
428
452
  * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
429
453
  *
430
- * const c = container().register(
454
+ * const c = Container.empty().register(
431
455
  * ConfigTag,
432
456
  * () => ({ apiUrl: 'https://api.example.com' })
433
457
  * );
@@ -435,12 +459,12 @@ var Container = class {
435
459
  *
436
460
  * @example With finalizer for cleanup
437
461
  * ```typescript
438
- * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
462
+ * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
439
463
  * async connect() { return; }
440
464
  * async close() { return; }
441
465
  * }
442
466
  *
443
- * const c = container().register(
467
+ * const c = Container.empty().register(
444
468
  * DatabaseConnection,
445
469
  * async () => {
446
470
  * const conn = new DatabaseConnection();
@@ -451,35 +475,43 @@ var Container = class {
451
475
  * );
452
476
  * ```
453
477
  */
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);
478
+ register(tag, spec) {
479
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot register dependencies on a destroyed container");
480
+ 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.`);
481
+ if (typeof spec === "function") {
482
+ this.factories.set(tag, spec);
483
+ this.finalizers.delete(tag);
484
+ } else {
485
+ this.factories.set(tag, spec.factory);
486
+ this.finalizers.set(tag, spec.finalizer);
460
487
  }
461
488
  return this;
462
489
  }
463
490
  /**
464
- * Checks if a dependency has been instantiated (cached) in the container.
491
+ * Checks if a dependency has been registered in the container.
465
492
  *
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`.
493
+ * This returns `true` if the dependency has been registered via `.register()`,
494
+ * regardless of whether it has been instantiated yet.
468
495
  *
469
496
  * @param tag - The dependency tag to check
470
- * @returns `true` if the dependency has been instantiated and cached, `false` otherwise
497
+ * @returns `true` if the dependency has been registered, `false` otherwise
471
498
  *
472
499
  * @example
473
500
  * ```typescript
474
- * 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
501
+ * const c = Container.empty().register(DatabaseService, () => new DatabaseService());
502
+ * console.log(c.has(DatabaseService)); // true
480
503
  * ```
481
504
  */
482
505
  has(tag) {
506
+ return this.factories.has(tag);
507
+ }
508
+ /**
509
+ * Checks if a dependency has been instantiated (cached) in the container.
510
+ *
511
+ * @param tag - The dependency tag to check
512
+ * @returns true if the dependency has been instantiated, false otherwise
513
+ */
514
+ exists(tag) {
483
515
  return this.cache.has(tag);
484
516
  }
485
517
  /**
@@ -501,10 +533,10 @@ var Container = class {
501
533
  *
502
534
  * @example Basic usage
503
535
  * ```typescript
504
- * const c = container()
536
+ * const c = Container.empty()
505
537
  * .register(DatabaseService, () => new DatabaseService());
506
538
  *
507
- * const db = await c.get(DatabaseService);
539
+ * const db = await c.resolve(DatabaseService);
508
540
  * db.query('SELECT * FROM users');
509
541
  * ```
510
542
  *
@@ -512,9 +544,9 @@ var Container = class {
512
544
  * ```typescript
513
545
  * // All three calls will receive the same instance
514
546
  * const [db1, db2, db3] = await Promise.all([
515
- * c.get(DatabaseService),
516
- * c.get(DatabaseService),
517
- * c.get(DatabaseService)
547
+ * c.resolve(DatabaseService),
548
+ * c.resolve(DatabaseService),
549
+ * c.resolve(DatabaseService)
518
550
  * ]);
519
551
  *
520
552
  * console.log(db1 === db2 === db3); // true
@@ -522,25 +554,141 @@ var Container = class {
522
554
  *
523
555
  * @example Dependency injection in factories
524
556
  * ```typescript
525
- * const c = container()
557
+ * const c = Container.empty()
526
558
  * .register(DatabaseService, () => new DatabaseService())
527
- * .register(UserService, async (container) => {
528
- * const db = await container.get(DatabaseService);
559
+ * .register(UserService, async (ctx) => {
560
+ * const db = await ctx.resolve(DatabaseService);
529
561
  * return new UserService(db);
530
562
  * });
531
563
  *
532
- * const userService = await c.get(UserService);
564
+ * const userService = await c.resolve(UserService);
565
+ * ```
566
+ */
567
+ async resolve(tag) {
568
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
569
+ const cached = this.cache.get(tag);
570
+ if (cached !== void 0) return cached;
571
+ const currentChain = resolutionChain.getStore() ?? [];
572
+ if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
573
+ const factory = this.factories.get(tag);
574
+ if (factory === void 0) throw new UnknownDependencyError(tag);
575
+ const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
576
+ try {
577
+ const instance = await factory(this);
578
+ return instance;
579
+ } catch (error) {
580
+ throw new DependencyCreationError(tag, error);
581
+ }
582
+ }).catch((error) => {
583
+ this.cache.delete(tag);
584
+ throw error;
585
+ });
586
+ this.cache.set(tag, instancePromise);
587
+ return instancePromise;
588
+ }
589
+ /**
590
+ * Resolves multiple dependencies concurrently using Promise.all.
591
+ *
592
+ * This method takes a variable number of dependency tags and resolves all of them concurrently,
593
+ * returning a tuple with the resolved instances in the same order as the input tags.
594
+ * The method maintains all the same guarantees as the individual resolve method:
595
+ * singleton behavior, circular dependency detection, and proper error handling.
596
+ *
597
+ * @template T - The tuple type of dependency tags to resolve
598
+ * @param tags - Variable number of dependency tags to resolve
599
+ * @returns Promise resolving to a tuple of service instances in the same order
600
+ * @throws {ContainerDestroyedError} If the container has been destroyed
601
+ * @throws {UnknownDependencyError} If any dependency is not registered
602
+ * @throws {CircularDependencyError} If a circular dependency is detected
603
+ * @throws {DependencyCreationError} If any factory function throws an error
604
+ *
605
+ * @example Basic usage
606
+ * ```typescript
607
+ * const c = Container.empty()
608
+ * .register(DatabaseService, () => new DatabaseService())
609
+ * .register(LoggerService, () => new LoggerService());
610
+ *
611
+ * const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
612
+ * ```
613
+ *
614
+ * @example Mixed tag types
615
+ * ```typescript
616
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
617
+ * const c = Container.empty()
618
+ * .register(ApiKeyTag, () => 'secret-key')
619
+ * .register(UserService, () => new UserService());
620
+ *
621
+ * const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
622
+ * ```
623
+ *
624
+ * @example Empty array
625
+ * ```typescript
626
+ * const results = await c.resolveAll(); // Returns empty array
533
627
  * ```
534
628
  */
535
- async get(tag) {
536
- return resolveDependency(tag, this.cache, this.factories, this);
629
+ async resolveAll(...tags) {
630
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
631
+ const promises = tags.map((tag) => this.resolve(tag));
632
+ const results = await Promise.all(promises);
633
+ return results;
634
+ }
635
+ /**
636
+ * Copies all registrations from this container to a target container.
637
+ *
638
+ * @internal
639
+ * @param target - The container to copy registrations to
640
+ * @throws {ContainerDestroyedError} If this container has been destroyed
641
+ */
642
+ copyTo(target) {
643
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot copy registrations from a destroyed container");
644
+ for (const [tag, factory] of this.factories) {
645
+ const finalizer = this.finalizers.get(tag);
646
+ if (finalizer) target.register(tag, {
647
+ factory,
648
+ finalizer
649
+ });
650
+ else target.register(tag, factory);
651
+ }
537
652
  }
538
653
  /**
539
- * Destroys all instantiated dependencies by calling their finalizers, then clears the instance cache.
654
+ * Creates a new container by merging this container's registrations with another container.
655
+ *
656
+ * This method creates a new container that contains all registrations from both containers.
657
+ * If there are conflicts (same dependency registered in both containers), this
658
+ * container's registration will take precedence.
659
+ *
660
+ * **Important**: Only the registrations are copied, not any cached instances.
661
+ * The new container starts with an empty instance cache.
540
662
  *
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.
663
+ * @param other - The container to merge with
664
+ * @returns A new container with combined registrations
665
+ * @throws {ContainerDestroyedError} If this container has been destroyed
666
+ *
667
+ * @example Merging containers
668
+ * ```typescript
669
+ * const container1 = Container.empty()
670
+ * .register(DatabaseService, () => new DatabaseService());
671
+ *
672
+ * const container2 = Container.empty()
673
+ * .register(UserService, () => new UserService());
674
+ *
675
+ * const merged = container1.merge(container2);
676
+ * // merged has both DatabaseService and UserService
677
+ * ```
678
+ */
679
+ merge(other) {
680
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
681
+ const merged = new Container();
682
+ other.copyTo(merged);
683
+ this.copyTo(merged);
684
+ return merged;
685
+ }
686
+ /**
687
+ * Destroys all instantiated dependencies by calling their finalizers and makes the container unusable.
688
+ *
689
+ * **Important: After calling destroy(), the container becomes permanently unusable.**
690
+ * Any subsequent calls to register(), get(), or destroy() will throw a ContainerError.
691
+ * This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
544
692
  *
545
693
  * All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
546
694
  * for maximum cleanup performance.
@@ -552,11 +700,11 @@ var Container = class {
552
700
  * dependencies are cleaned up.
553
701
  *
554
702
  * @returns Promise that resolves when all cleanup is complete
555
- * @throws {DependencyContainerFinalizationError} If any finalizers fail during cleanup
703
+ * @throws {DependencyFinalizationError} If any finalizers fail during cleanup
556
704
  *
557
- * @example Basic cleanup and reuse
705
+ * @example Basic cleanup
558
706
  * ```typescript
559
- * const c = container()
707
+ * const c = Container.empty()
560
708
  * .register(DatabaseConnection,
561
709
  * async () => {
562
710
  * const conn = new DatabaseConnection();
@@ -566,24 +714,32 @@ var Container = class {
566
714
  * (conn) => conn.disconnect() // Finalizer
567
715
  * );
568
716
  *
569
- * // First use cycle
570
- * const db1 = await c.get(DatabaseConnection);
571
- * await c.destroy(); // Calls conn.disconnect(), clears cache
717
+ * const db = await c.resolve(DatabaseConnection);
718
+ * await c.destroy(); // Calls conn.disconnect(), container becomes unusable
572
719
  *
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
720
+ * // This will throw an error
721
+ * try {
722
+ * await c.resolve(DatabaseConnection);
723
+ * } catch (error) {
724
+ * console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
725
+ * }
576
726
  * ```
577
727
  *
578
- * @example Multiple destroy/reuse cycles
728
+ * @example Application shutdown
579
729
  * ```typescript
580
- * const c = container().register(UserService, () => new UserService());
730
+ * const appContainer Container.empty
731
+ * .register(DatabaseService, () => new DatabaseService())
732
+ * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
581
733
  *
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
- * }
734
+ * // During application shutdown
735
+ * process.on('SIGTERM', async () => {
736
+ * try {
737
+ * await appContainer.destroy(); // Clean shutdown of all services
738
+ * } catch (error) {
739
+ * console.error('Error during shutdown:', error);
740
+ * }
741
+ * process.exit(0);
742
+ * });
587
743
  * ```
588
744
  *
589
745
  * @example Handling cleanup errors
@@ -595,92 +751,196 @@ var Container = class {
595
751
  * console.error('Some dependencies failed to clean up:', error.detail.errors);
596
752
  * }
597
753
  * }
598
- * // Container is still reusable even after finalizer errors
754
+ * // Container is destroyed regardless of finalizer errors
599
755
  * ```
600
756
  */
601
757
  async destroy() {
758
+ if (this.isDestroyed) return;
602
759
  try {
603
- await runFinalizers(this.finalizers, this.cache);
760
+ const promises = Array.from(this.finalizers.entries()).filter(([tag]) => this.cache.has(tag)).map(async ([tag, finalizer]) => {
761
+ const dep = await this.cache.get(tag);
762
+ return finalizer(dep);
763
+ });
764
+ const results = await Promise.allSettled(promises);
765
+ const failures = results.filter((result) => result.status === "rejected");
766
+ if (failures.length > 0) throw new DependencyFinalizationError(failures.map((result) => result.reason));
604
767
  } finally {
768
+ this.isDestroyed = true;
605
769
  this.cache.clear();
606
770
  }
607
771
  }
608
772
  };
609
- var ScopedContainer = class ScopedContainer {
773
+
774
+ //#endregion
775
+ //#region src/layer.ts
776
+ /**
777
+ * The type ID for the Layer interface.
778
+ */
779
+ const LayerTypeId = Symbol.for("sandly/Layer");
780
+ /**
781
+ * Creates a new dependency layer that encapsulates a set of dependency registrations.
782
+ * Layers are the primary building blocks for organizing and composing dependency injection setups.
783
+ *
784
+ * @template TRequires - The union of dependency tags this layer requires from other layers or external setup
785
+ * @template TProvides - The union of dependency tags this layer registers/provides
786
+ *
787
+ * @param register - Function that performs the dependency registrations. Receives a container.
788
+ * @returns The layer instance.
789
+ *
790
+ * @example Simple layer
791
+ * ```typescript
792
+ * import { layer, Tag } from 'sandly';
793
+ *
794
+ * class DatabaseService extends Tag.Service('DatabaseService') {
795
+ * constructor(private url: string = 'sqlite://memory') {}
796
+ * query() { return 'data'; }
797
+ * }
798
+ *
799
+ * // Layer that provides DatabaseService, requires nothing
800
+ * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
801
+ * container.register(DatabaseService, () => new DatabaseService())
802
+ * );
803
+ *
804
+ * // Usage
805
+ * const dbLayerInstance = databaseLayer;
806
+ * ```
807
+ *
808
+ * @example Complex application layer structure
809
+ * ```typescript
810
+ * // Configuration layer
811
+ * const configLayer = layer<never, typeof ConfigTag>((container) =>
812
+ * container.register(ConfigTag, () => loadConfig())
813
+ * );
814
+ *
815
+ * // Infrastructure layer (requires config)
816
+ * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
817
+ * (container) =>
818
+ * container
819
+ * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
820
+ * .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
821
+ * );
822
+ *
823
+ * // Service layer (requires infrastructure)
824
+ * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
825
+ * (container) =>
826
+ * container.register(UserService, async (ctx) =>
827
+ * new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
828
+ * )
829
+ * );
830
+ *
831
+ * // Compose the complete application
832
+ * const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
833
+ * ```
834
+ */
835
+ function layer(register) {
836
+ const layerImpl = {
837
+ register: (container) => register(container),
838
+ provide(dependency) {
839
+ return createProvidedLayer(dependency, layerImpl);
840
+ },
841
+ provideMerge(dependency) {
842
+ return createComposedLayer(dependency, layerImpl);
843
+ },
844
+ merge(other) {
845
+ return createMergedLayer(layerImpl, other);
846
+ }
847
+ };
848
+ return layerImpl;
849
+ }
850
+ /**
851
+ * Internal function to create a provided layer from two layers.
852
+ * This implements the `.provide()` method logic - only exposes target layer's provisions.
853
+ *
854
+ * @internal
855
+ */
856
+ function createProvidedLayer(dependency, target) {
857
+ return createComposedLayer(dependency, target);
858
+ }
859
+ /**
860
+ * Internal function to create a composed layer from two layers.
861
+ * This implements the `.provideMerge()` method logic - exposes both layers' provisions.
862
+ *
863
+ * @internal
864
+ */
865
+ function createComposedLayer(dependency, target) {
866
+ return layer((container) => {
867
+ const containerWithDependency = dependency.register(container);
868
+ return target.register(containerWithDependency);
869
+ });
870
+ }
871
+ /**
872
+ * Internal function to create a merged layer from two layers.
873
+ * This implements the `.merge()` method logic.
874
+ *
875
+ * @internal
876
+ */
877
+ function createMergedLayer(layer1, layer2) {
878
+ return layer((container) => {
879
+ const container1 = layer1.register(container);
880
+ const container2 = layer2.register(container1);
881
+ return container2;
882
+ });
883
+ }
884
+ /**
885
+ * Utility object containing helper functions for working with layers.
886
+ */
887
+ const Layer = {
888
+ empty() {
889
+ return layer((container) => container);
890
+ },
891
+ mergeAll(...layers) {
892
+ return layers.reduce((acc, layer$1) => acc.merge(layer$1));
893
+ },
894
+ merge(layer1, layer2) {
895
+ return layer1.merge(layer2);
896
+ }
897
+ };
898
+
899
+ //#endregion
900
+ //#region src/scoped-container.ts
901
+ var ScopedContainer = class ScopedContainer extends Container {
610
902
  scope;
611
903
  parent;
612
904
  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
905
  constructor(parent, scope) {
906
+ super();
629
907
  this.parent = parent;
630
908
  this.scope = scope;
631
909
  }
910
+ static empty(scope) {
911
+ return new ScopedContainer(null, scope);
912
+ }
632
913
  /**
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());
914
+ * Registers a dependency in the scoped container.
654
915
  *
655
- * // Register in runtime scope from request container - delegates to parent
656
- * request.register(DatabaseService, () => new DatabaseService(), undefined, 'runtime');
657
- * ```
916
+ * Overrides the base implementation to return ScopedContainer type
917
+ * for proper method chaining support.
658
918
  */
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);
919
+ register(tag, spec) {
920
+ super.register(tag, spec);
671
921
  return this;
672
922
  }
673
923
  /**
674
- * Checks if a dependency has been instantiated in this scope or any parent scope.
924
+ * Checks if a dependency has been registered in this scope or any parent scope.
675
925
  *
676
926
  * 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.
927
+ * Returns true if the dependency has been registered somewhere in the scope hierarchy.
678
928
  */
679
929
  has(tag) {
680
- if (this.cache.has(tag)) return true;
930
+ if (super.has(tag)) return true;
681
931
  return this.parent?.has(tag) ?? false;
682
932
  }
683
933
  /**
934
+ * Checks if a dependency has been instantiated in this scope or any parent scope.
935
+ *
936
+ * This method checks the current scope first, then walks up the parent chain.
937
+ * Returns true if the dependency has been instantiated somewhere in the scope hierarchy.
938
+ */
939
+ exists(tag) {
940
+ if (super.exists(tag)) return true;
941
+ return this.parent?.exists(tag) ?? false;
942
+ }
943
+ /**
684
944
  * Retrieves a dependency instance, resolving from the current scope or parent scopes.
685
945
  *
686
946
  * Resolution strategy:
@@ -689,9 +949,9 @@ var ScopedContainer = class ScopedContainer {
689
949
  * 3. Otherwise, delegate to parent scope
690
950
  * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
691
951
  */
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);
952
+ async resolve(tag) {
953
+ if (this.factories.has(tag)) return super.resolve(tag);
954
+ if (this.parent !== null) return this.parent.resolve(tag);
695
955
  throw new UnknownDependencyError(tag);
696
956
  }
697
957
  /**
@@ -706,19 +966,38 @@ var ScopedContainer = class ScopedContainer {
706
966
  * before their dependents.
707
967
  */
708
968
  async destroy() {
969
+ if (this.isDestroyed) return;
709
970
  const allFailures = [];
971
+ const childDestroyPromises = this.children.map((weakRef) => weakRef.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
972
+ const childResults = await Promise.allSettled(childDestroyPromises);
973
+ const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
974
+ allFailures.push(...childFailures);
710
975
  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);
976
+ await super.destroy();
716
977
  } catch (error) {
717
978
  allFailures.push(error);
718
979
  } finally {
719
- this.cache.clear();
980
+ this.parent = null;
720
981
  }
721
- if (allFailures.length > 0) throw new DependencyContainerFinalizationError(allFailures);
982
+ if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
983
+ }
984
+ /**
985
+ * Creates a new scoped container by merging this container's registrations with another container.
986
+ *
987
+ * This method overrides the base Container.merge to return a ScopedContainer instead of a regular Container.
988
+ * The resulting scoped container contains all registrations from both containers and becomes a root scope
989
+ * (no parent) with the scope name from this container.
990
+ *
991
+ * @param other - The container to merge with
992
+ * @returns A new ScopedContainer with combined registrations
993
+ * @throws {ContainerDestroyedError} If this container has been destroyed
994
+ */
995
+ merge(other) {
996
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot merge from a destroyed container");
997
+ const merged = new ScopedContainer(null, this.scope);
998
+ other.copyTo(merged);
999
+ this.copyTo(merged);
1000
+ return merged;
722
1001
  }
723
1002
  /**
724
1003
  * Creates a child scoped container.
@@ -727,203 +1006,79 @@ var ScopedContainer = class ScopedContainer {
727
1006
  * their own scope for new registrations and instance caching.
728
1007
  */
729
1008
  child(scope) {
1009
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
730
1010
  const child = new ScopedContainer(this, scope);
731
- this.children.push(child);
1011
+ this.children.push(new WeakRef(child));
732
1012
  return child;
733
1013
  }
734
1014
  };
735
1015
  /**
736
- * Creates a new empty dependency injection container.
737
- *
738
- * This is a convenience factory function that creates a new DependencyContainer instance.
739
- * The returned container starts with no registered dependencies and the type parameter
740
- * defaults to `never`, indicating no dependencies are available for retrieval yet.
741
- *
742
- * @returns A new empty DependencyContainer instance
743
- *
744
- * @example
745
- * ```typescript
746
- * import { container, Tag } from 'sandl';
747
- *
748
- * class DatabaseService extends Tag.Class('DatabaseService') {}
749
- * class UserService extends Tag.Class('UserService') {}
750
- *
751
- * const c = container()
752
- * .register(DatabaseService, () => new DatabaseService())
753
- * .register(UserService, async (container) =>
754
- * new UserService(await container.get(DatabaseService))
755
- * );
756
- *
757
- * const userService = await c.get(UserService);
758
- * ```
759
- */
760
- function container() {
761
- return new Container();
762
- }
763
- function scopedContainer(scope) {
764
- return new ScopedContainer(null, scope);
765
- }
766
-
767
- //#endregion
768
- //#region src/layer.ts
769
- /**
770
- * Creates a new dependency layer that encapsulates a set of dependency registrations.
771
- * Layers are the primary building blocks for organizing and composing dependency injection setups.
772
- *
773
- * @template TRequires - The union of dependency tags this layer requires from other layers or external setup
774
- * @template TProvides - The union of dependency tags this layer registers/provides
775
- * @template TParams - Optional parameters that can be passed to configure the layer
1016
+ * Converts a regular container into a scoped container, copying all registrations.
776
1017
  *
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.
1018
+ * This function creates a new ScopedContainer instance and copies all factory functions
1019
+ * and finalizers from the source container. The resulting scoped container becomes a root
1020
+ * scope (no parent) with all the same dependency registrations.
779
1021
  *
780
- * @example Simple layer without parameters
781
- * ```typescript
782
- * import { layer, Tag } from '@/di/layer.js';
783
- *
784
- * class DatabaseService extends Tag.Class('DatabaseService') {
785
- * constructor(private url: string = 'sqlite://memory') {}
786
- * query() { return 'data'; }
787
- * }
788
- *
789
- * // Layer that provides DatabaseService, requires nothing
790
- * const databaseLayer = layer<never, typeof DatabaseService>((container) =>
791
- * container.register(DatabaseService, () => new DatabaseService())
792
- * );
1022
+ * **Important**: Only the registrations are copied, not any cached instances.
1023
+ * The new scoped container starts with an empty instance cache.
793
1024
  *
794
- * // Usage
795
- * const dbLayerInstance = databaseLayer(); // No parameters needed
796
- * ```
1025
+ * @param container - The container to convert to a scoped container
1026
+ * @param scope - A string or symbol identifier for this scope (used for debugging)
1027
+ * @returns A new ScopedContainer instance with all registrations copied from the source container
1028
+ * @throws {ContainerDestroyedError} If the source container has been destroyed
797
1029
  *
798
- * @example Layer with dependencies
1030
+ * @example Converting a regular container to scoped
799
1031
  * ```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
- * ```
1032
+ * import { container, scoped } from 'sandly';
810
1033
  *
811
- * @example Parameterized layer
812
- * ```typescript
813
- * interface DatabaseConfig {
814
- * host: string;
815
- * port: number;
816
- * }
1034
+ * const appContainer = Container.empty()
1035
+ * .register(DatabaseService, () => new DatabaseService())
1036
+ * .register(ConfigService, () => new ConfigService());
817
1037
  *
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
- * );
1038
+ * const scopedAppContainer = scoped(appContainer, 'app');
823
1039
  *
824
- * // Usage with parameters
825
- * const dbLayerInstance = databaseLayer({ host: 'localhost', port: 5432 });
1040
+ * // Create child scopes
1041
+ * const requestContainer = scopedAppContainer.child('request');
826
1042
  * ```
827
1043
  *
828
- * @example Complex application layer structure
1044
+ * @example Copying complex registrations
829
1045
  * ```typescript
830
- * // Configuration layer
831
- * const configLayer = layer<never, typeof ConfigTag>((container) =>
832
- * container.register(ConfigTag, () => loadConfig())
833
- * );
834
- *
835
- * // Infrastructure layer (requires config)
836
- * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
837
- * (container) =>
838
- * container
839
- * .register(DatabaseService, async (c) => new DatabaseService(await c.get(ConfigTag)))
840
- * .register(CacheService, async (c) => new CacheService(await c.get(ConfigTag)))
841
- * );
842
- *
843
- * // Service layer (requires infrastructure)
844
- * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
845
- * (container) =>
846
- * container.register(UserService, async (c) =>
847
- * new UserService(await c.get(DatabaseService), await c.get(CacheService))
848
- * )
849
- * );
1046
+ * const baseContainer = Container.empty()
1047
+ * .register(DatabaseService, () => new DatabaseService())
1048
+ * .register(UserService, {
1049
+ * factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
1050
+ * finalizer: (service) => service.cleanup()
1051
+ * });
850
1052
  *
851
- * // Compose the complete application
852
- * const appLayer = configLayer().to(infraLayer()).to(serviceLayer());
1053
+ * const scopedContainer = scoped(baseContainer, 'app');
1054
+ * // scopedContainer now has all the same registrations with finalizers preserved
853
1055
  * ```
854
1056
  */
855
- 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;
867
- };
868
- return factory;
869
- }
870
- /**
871
- * Internal function to create a composed layer from two layers.
872
- * This implements the `.to()` method logic.
873
- *
874
- * @internal
875
- */
876
- function createComposedLayer(source, target) {
877
- return layer((container$1) => {
878
- const containerWithSource = source.register(container$1);
879
- return target.register(containerWithSource);
880
- })();
881
- }
882
- /**
883
- * Internal function to create a merged layer from two layers.
884
- * This implements the `.and()` method logic.
885
- *
886
- * @internal
887
- */
888
- function createMergedLayer(layer1, layer2) {
889
- return layer((container$1) => {
890
- const container1 = layer1.register(container$1);
891
- return layer2.register(container1);
892
- })();
1057
+ function scoped(container, scope) {
1058
+ const emptyScoped = ScopedContainer.empty(scope);
1059
+ return emptyScoped.merge(container);
893
1060
  }
894
- /**
895
- * Utility object containing helper functions for working with layers.
896
- */
897
- const Layer = {
898
- empty() {
899
- return layer((container$1) => container$1)();
900
- },
901
- merge(...layers) {
902
- return layers.reduce((acc, layer$1) => acc.and(layer$1));
903
- }
904
- };
905
1061
 
906
1062
  //#endregion
907
1063
  //#region src/service.ts
908
1064
  /**
909
- * Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
1065
+ * Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
910
1066
  *
911
- * For ClassTag services:
1067
+ * For ServiceTag services:
912
1068
  * - Dependencies are automatically inferred from constructor parameters
913
1069
  * - The factory function must handle dependency injection by resolving dependencies from the container
914
1070
  *
915
1071
  * For ValueTag services:
916
1072
  * - No constructor dependencies are needed since they don't have constructors
917
1073
  *
918
- * @template T - The tag representing the service (ClassTag or ValueTag)
919
- * @template TParams - Optional parameters for service configuration
920
- * @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
1074
+ * @template T - The tag representing the service (ServiceTag or ValueTag)
1075
+ * @param tag - The tag (ServiceTag or ValueTag)
1076
+ * @param factory - Factory function for service instantiation with container
1077
+ * @returns The service layer
923
1078
  *
924
1079
  * @example Simple service without dependencies
925
1080
  * ```typescript
926
- * class LoggerService extends Tag.Class('LoggerService') {
1081
+ * class LoggerService extends Tag.Service('LoggerService') {
927
1082
  * log(message: string) { console.log(message); }
928
1083
  * }
929
1084
  *
@@ -932,11 +1087,11 @@ const Layer = {
932
1087
  *
933
1088
  * @example Service with dependencies
934
1089
  * ```typescript
935
- * class DatabaseService extends Tag.Class('DatabaseService') {
1090
+ * class DatabaseService extends Tag.Service('DatabaseService') {
936
1091
  * query() { return []; }
937
1092
  * }
938
1093
  *
939
- * class UserService extends Tag.Class('UserService') {
1094
+ * class UserService extends Tag.Service('UserService') {
940
1095
  * constructor(private db: DatabaseService) {
941
1096
  * super();
942
1097
  * }
@@ -944,40 +1099,40 @@ const Layer = {
944
1099
  * getUsers() { return this.db.query(); }
945
1100
  * }
946
1101
  *
947
- * const userService = service(UserService, async (container) =>
948
- * new UserService(await container.get(DatabaseService))
1102
+ * const userService = service(UserService, async (ctx) =>
1103
+ * new UserService(await ctx.resolve(DatabaseService))
949
1104
  * );
950
1105
  * ```
1106
+ */
1107
+ function service(tag, spec) {
1108
+ return layer((container) => {
1109
+ return container.register(tag, spec);
1110
+ });
1111
+ }
1112
+
1113
+ //#endregion
1114
+ //#region src/value.ts
1115
+ /**
1116
+ * Creates a layer that provides a constant value for a given tag.
951
1117
  *
952
- * @example Service with configuration parameters
1118
+ * @param tag - The value tag to provide
1119
+ * @param constantValue - The constant value to provide
1120
+ * @returns A layer with no dependencies that provides the constant value
1121
+ *
1122
+ * @example
953
1123
  * ```typescript
954
- * class DatabaseService extends Tag.Class('DatabaseService') {
955
- * constructor(private config: { dbUrl: string }) {
956
- * super();
957
- * }
958
- * }
1124
+ * const ApiKey = Tag.of('ApiKey')<string>();
1125
+ * const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
959
1126
  *
960
- * const dbService = service(
961
- * DatabaseService,
962
- * (container, params: { dbUrl: string }) => new DatabaseService(params)
963
- * );
1127
+ * const apiKey = value(ApiKey, 'my-secret-key');
1128
+ * const dbUrl = value(DatabaseUrl, 'postgresql://localhost:5432/myapp');
1129
+ *
1130
+ * const config = Layer.merge(apiKey, dbUrl);
964
1131
  * ```
965
1132
  */
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;
1133
+ function value(tag, constantValue) {
1134
+ return layer((container) => container.register(tag, () => constantValue));
980
1135
  }
981
1136
 
982
1137
  //#endregion
983
- export { Layer, Tag, container, layer, scopedContainer, service };
1138
+ export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, InjectSource, Layer, ScopedContainer, Tag, UnknownDependencyError, layer, scoped, service, value };