sandly 0.0.2

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.
package/dist/index.js ADDED
@@ -0,0 +1,983 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ //#region src/tag.ts
4
+ /**
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.
7
+ * @internal
8
+ */
9
+ const TagId = "__tag_id__";
10
+ /**
11
+ * Utility object containing factory functions for creating dependency tags.
12
+ *
13
+ * The Tag object provides the primary API for creating both value tags and class tags
14
+ * used throughout the dependency injection system. It's the main entry point for
15
+ * defining dependencies in a type-safe way.
16
+ */
17
+ const Tag = {
18
+ of: (id) => {
19
+ return () => ({
20
+ [TagId]: id,
21
+ __type: void 0
22
+ });
23
+ },
24
+ for: () => {
25
+ return {
26
+ [TagId]: Symbol(),
27
+ __type: void 0
28
+ };
29
+ },
30
+ Class: (id) => {
31
+ class Tagged {
32
+ static [TagId] = id;
33
+ [TagId] = id;
34
+ /** @internal */
35
+ __type;
36
+ }
37
+ return Tagged;
38
+ },
39
+ 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);
46
+ }
47
+ };
48
+
49
+ //#endregion
50
+ //#region src/errors.ts
51
+ var BaseError = class BaseError extends Error {
52
+ detail;
53
+ constructor(message, { cause, detail } = {}) {
54
+ super(message, { cause });
55
+ this.name = this.constructor.name;
56
+ this.detail = detail;
57
+ if (cause instanceof Error && cause.stack !== void 0) this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
58
+ }
59
+ static ensure(error) {
60
+ return error instanceof BaseError ? error : new BaseError("An unknown error occurred", { cause: error });
61
+ }
62
+ dump() {
63
+ const cause = this.cause instanceof BaseError ? this.cause.dump().error : this.cause;
64
+ const result = {
65
+ name: this.name,
66
+ message: this.message,
67
+ cause,
68
+ detail: this.detail ?? {}
69
+ };
70
+ return {
71
+ name: this.name,
72
+ message: result.message,
73
+ stack: this.stack,
74
+ error: result
75
+ };
76
+ }
77
+ dumps() {
78
+ return JSON.stringify(this.dump());
79
+ }
80
+ };
81
+ /**
82
+ * Base error class for all dependency container related errors.
83
+ *
84
+ * This extends the framework's BaseError to provide consistent error handling
85
+ * and structured error information across the dependency injection system.
86
+ *
87
+ * @example Catching DI errors
88
+ * ```typescript
89
+ * try {
90
+ * await container.get(SomeService);
91
+ * } catch (error) {
92
+ * if (error instanceof DependencyContainerError) {
93
+ * console.error('DI Error:', error.message);
94
+ * console.error('Details:', error.detail);
95
+ * }
96
+ * }
97
+ * ```
98
+ */
99
+ var DependencyContainerError = class extends BaseError {};
100
+ /**
101
+ * Error thrown when attempting to retrieve a dependency that hasn't been registered.
102
+ *
103
+ * This error occurs when calling `container.get(Tag)` for a tag that was never
104
+ * registered via `container.register()`. It indicates a programming error where
105
+ * the dependency setup is incomplete.
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const c = container(); // Empty container
110
+ *
111
+ * try {
112
+ * await c.get(UnregisteredService); // This will throw
113
+ * } catch (error) {
114
+ * if (error instanceof UnknownDependencyError) {
115
+ * console.error('Missing dependency:', error.message);
116
+ * }
117
+ * }
118
+ * ```
119
+ */
120
+ var UnknownDependencyError = class extends DependencyContainerError {
121
+ /**
122
+ * @internal
123
+ * Creates an UnknownDependencyError for the given tag.
124
+ *
125
+ * @param tag - The dependency tag that wasn't found
126
+ */
127
+ constructor(tag) {
128
+ super(`No factory registered for dependency ${Tag.id(tag)}`);
129
+ }
130
+ };
131
+ /**
132
+ * Error thrown when a circular dependency is detected during dependency resolution.
133
+ *
134
+ * This occurs when service A depends on service B, which depends on service A (directly
135
+ * or through a chain of dependencies). The error includes the full dependency chain
136
+ * to help identify the circular reference.
137
+ *
138
+ * @example Circular dependency scenario
139
+ * ```typescript
140
+ * class ServiceA extends Tag.Class('ServiceA') {}
141
+ * class ServiceB extends Tag.Class('ServiceB') {}
142
+ *
143
+ * const c = container()
144
+ * .register(ServiceA, async (container) =>
145
+ * new ServiceA(await container.get(ServiceB)) // Depends on B
146
+ * )
147
+ * .register(ServiceB, async (container) =>
148
+ * new ServiceB(await container.get(ServiceA)) // Depends on A - CIRCULAR!
149
+ * );
150
+ *
151
+ * try {
152
+ * await c.get(ServiceA);
153
+ * } catch (error) {
154
+ * if (error instanceof CircularDependencyError) {
155
+ * console.error('Circular dependency:', error.message);
156
+ * // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
157
+ * }
158
+ * }
159
+ * ```
160
+ */
161
+ var CircularDependencyError = class extends DependencyContainerError {
162
+ /**
163
+ * @internal
164
+ * Creates a CircularDependencyError with the dependency chain information.
165
+ *
166
+ * @param tag - The tag where the circular dependency was detected
167
+ * @param dependencyChain - The chain of dependencies that led to the circular reference
168
+ */
169
+ constructor(tag, dependencyChain) {
170
+ const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
171
+ super(`Circular dependency detected for ${Tag.id(tag)}: ${chain} -> ${Tag.id(tag)}`, { detail: {
172
+ tag: Tag.id(tag),
173
+ dependencyChain: dependencyChain.map((t) => Tag.id(t))
174
+ } });
175
+ }
176
+ };
177
+ /**
178
+ * Error thrown when a dependency factory function throws an error during instantiation.
179
+ *
180
+ * This wraps the original error with additional context about which dependency
181
+ * failed to be created. The original error is preserved as the `cause` property.
182
+ *
183
+ * @example Factory throwing error
184
+ * ```typescript
185
+ * class DatabaseService extends Tag.Class('DatabaseService') {}
186
+ *
187
+ * const c = container().register(DatabaseService, () => {
188
+ * throw new Error('Database connection failed');
189
+ * });
190
+ *
191
+ * try {
192
+ * await c.get(DatabaseService);
193
+ * } catch (error) {
194
+ * if (error instanceof DependencyCreationError) {
195
+ * console.error('Failed to create:', error.message);
196
+ * console.error('Original error:', error.cause);
197
+ * }
198
+ * }
199
+ * ```
200
+ */
201
+ var DependencyCreationError = class extends DependencyContainerError {
202
+ /**
203
+ * @internal
204
+ * Creates a DependencyCreationError wrapping the original factory error.
205
+ *
206
+ * @param tag - The tag of the dependency that failed to be created
207
+ * @param error - The original error thrown by the factory function
208
+ */
209
+ constructor(tag, error) {
210
+ super(`Error creating instance of ${Tag.id(tag)}: ${error}`, {
211
+ cause: error,
212
+ detail: { tag: Tag.id(tag) }
213
+ });
214
+ }
215
+ };
216
+ /**
217
+ * Error thrown when one or more finalizers fail during container destruction.
218
+ *
219
+ * This error aggregates multiple finalizer failures that occurred during
220
+ * `container.destroy()`. Even if some finalizers fail, the container cleanup
221
+ * process continues and this error contains details of all failures.
222
+ *
223
+ * @example Handling finalization errors
224
+ * ```typescript
225
+ * try {
226
+ * await container.destroy();
227
+ * } catch (error) {
228
+ * if (error instanceof DependencyContainerFinalizationError) {
229
+ * console.error('Some finalizers failed');
230
+ * console.error('Error details:', error.detail.errors);
231
+ * }
232
+ * }
233
+ * ```
234
+ */
235
+ var DependencyContainerFinalizationError = class extends DependencyContainerError {
236
+ /**
237
+ * @internal
238
+ * Creates a DependencyContainerFinalizationError aggregating multiple finalizer failures.
239
+ *
240
+ * @param errors - Array of errors thrown by individual finalizers
241
+ */
242
+ constructor(errors) {
243
+ const lambdaErrors = errors.map((error) => BaseError.ensure(error));
244
+ super("Error destroying dependency container", {
245
+ cause: errors[0],
246
+ detail: { errors: lambdaErrors.map((error) => error.dump()) }
247
+ });
248
+ }
249
+ };
250
+
251
+ //#endregion
252
+ //#region src/container.ts
253
+ /**
254
+ * AsyncLocalStorage instance used to track the dependency resolution chain.
255
+ * This enables detection of circular dependencies during async dependency resolution.
256
+ * @internal
257
+ */
258
+ 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
+ }
299
+ /**
300
+ * A type-safe dependency injection container that manages service instantiation,
301
+ * caching, and lifecycle management with support for async dependencies and
302
+ * circular dependency detection.
303
+ *
304
+ * The container maintains complete type safety by tracking registered dependencies
305
+ * at the type level, ensuring that only registered dependencies can be retrieved
306
+ * and preventing runtime errors.
307
+ *
308
+ * @template TReg - Union type of all registered dependency tags in this container
309
+ *
310
+ * @example Basic usage with class tags
311
+ * ```typescript
312
+ * import { container, Tag } from 'sandl';
313
+ *
314
+ * class DatabaseService extends Tag.Class('DatabaseService') {
315
+ * query() { return 'data'; }
316
+ * }
317
+ *
318
+ * class UserService extends Tag.Class('UserService') {
319
+ * constructor(private db: DatabaseService) {}
320
+ * getUser() { return this.db.query(); }
321
+ * }
322
+ *
323
+ * const c = container()
324
+ * .register(DatabaseService, () => new DatabaseService())
325
+ * .register(UserService, async (container) =>
326
+ * new UserService(await container.get(DatabaseService))
327
+ * );
328
+ *
329
+ * const userService = await c.get(UserService);
330
+ * ```
331
+ *
332
+ * @example Usage with value tags
333
+ * ```typescript
334
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
335
+ * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
336
+ *
337
+ * const c = container()
338
+ * .register(ApiKeyTag, () => process.env.API_KEY!)
339
+ * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
340
+ *
341
+ * const apiKey = await c.get(ApiKeyTag);
342
+ * const config = await c.get(ConfigTag);
343
+ * ```
344
+ *
345
+ * @example With finalizers for cleanup
346
+ * ```typescript
347
+ * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
348
+ * async connect() { return; }
349
+ * async disconnect() { return; }
350
+ * }
351
+ *
352
+ * const c = container().register(
353
+ * DatabaseConnection,
354
+ * async () => {
355
+ * const conn = new DatabaseConnection();
356
+ * await conn.connect();
357
+ * return conn;
358
+ * },
359
+ * async (conn) => conn.disconnect() // Finalizer for cleanup
360
+ * );
361
+ *
362
+ * // Later...
363
+ * await c.destroy(); // Calls all finalizers
364
+ * ```
365
+ */
366
+ var Container = class {
367
+ /**
368
+ * Cache of instantiated dependencies as promises.
369
+ * Ensures singleton behavior and supports concurrent access.
370
+ * @internal
371
+ */
372
+ cache = /* @__PURE__ */ new Map();
373
+ /**
374
+ * Factory functions for creating dependency instances.
375
+ * @internal
376
+ */
377
+ factories = /* @__PURE__ */ new Map();
378
+ /**
379
+ * Finalizer functions for cleaning up dependencies when the container is destroyed.
380
+ * @internal
381
+ */
382
+ finalizers = /* @__PURE__ */ new Map();
383
+ /**
384
+ * Registers a dependency in the container with a factory function and optional finalizer.
385
+ *
386
+ * The factory function receives the current container instance and must return the
387
+ * service instance (or a Promise of it). The container tracks the registration at
388
+ * the type level, ensuring type safety for subsequent `.get()` calls.
389
+ *
390
+ * @template T - The dependency tag being registered
391
+ * @param tag - The dependency tag (class or value tag)
392
+ * @param factory - Function that creates the service instance, receives container for dependency injection
393
+ * @param finalizer - Optional cleanup function called when container is destroyed
394
+ * @returns A new container instance with the dependency registered
395
+ * @throws {DependencyContainerError} If the dependency is already registered
396
+ *
397
+ * @example Registering a simple service
398
+ * ```typescript
399
+ * class LoggerService extends Tag.Class('LoggerService') {
400
+ * log(message: string) { console.log(message); }
401
+ * }
402
+ *
403
+ * const c = container().register(
404
+ * LoggerService,
405
+ * () => new LoggerService()
406
+ * );
407
+ * ```
408
+ *
409
+ * @example Registering with dependencies
410
+ * ```typescript
411
+ * class UserService extends Tag.Class('UserService') {
412
+ * constructor(private db: DatabaseService, private logger: LoggerService) {}
413
+ * }
414
+ *
415
+ * const c = container()
416
+ * .register(DatabaseService, () => new DatabaseService())
417
+ * .register(LoggerService, () => new LoggerService())
418
+ * .register(UserService, async (container) =>
419
+ * new UserService(
420
+ * await container.get(DatabaseService),
421
+ * await container.get(LoggerService)
422
+ * )
423
+ * );
424
+ * ```
425
+ *
426
+ * @example Using value tags
427
+ * ```typescript
428
+ * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
429
+ *
430
+ * const c = container().register(
431
+ * ConfigTag,
432
+ * () => ({ apiUrl: 'https://api.example.com' })
433
+ * );
434
+ * ```
435
+ *
436
+ * @example With finalizer for cleanup
437
+ * ```typescript
438
+ * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
439
+ * async connect() { return; }
440
+ * async close() { return; }
441
+ * }
442
+ *
443
+ * const c = container().register(
444
+ * DatabaseConnection,
445
+ * async () => {
446
+ * const conn = new DatabaseConnection();
447
+ * await conn.connect();
448
+ * return conn;
449
+ * },
450
+ * (conn) => conn.close() // Called during container.destroy()
451
+ * );
452
+ * ```
453
+ */
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);
460
+ }
461
+ return this;
462
+ }
463
+ /**
464
+ * Checks if a dependency has been instantiated (cached) in the container.
465
+ *
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`.
468
+ *
469
+ * @param tag - The dependency tag to check
470
+ * @returns `true` if the dependency has been instantiated and cached, `false` otherwise
471
+ *
472
+ * @example
473
+ * ```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
480
+ * ```
481
+ */
482
+ has(tag) {
483
+ return this.cache.has(tag);
484
+ }
485
+ /**
486
+ * Retrieves a dependency instance from the container, creating it if necessary.
487
+ *
488
+ * This method ensures singleton behavior - each dependency is created only once
489
+ * and cached for subsequent calls. The method is async-safe and handles concurrent
490
+ * requests for the same dependency correctly.
491
+ *
492
+ * The method performs circular dependency detection using AsyncLocalStorage to track
493
+ * the resolution chain across async boundaries.
494
+ *
495
+ * @template T - The dependency tag type (must be registered in this container)
496
+ * @param tag - The dependency tag to retrieve
497
+ * @returns Promise resolving to the service instance
498
+ * @throws {UnknownDependencyError} If the dependency is not registered
499
+ * @throws {CircularDependencyError} If a circular dependency is detected
500
+ * @throws {DependencyCreationError} If the factory function throws an error
501
+ *
502
+ * @example Basic usage
503
+ * ```typescript
504
+ * const c = container()
505
+ * .register(DatabaseService, () => new DatabaseService());
506
+ *
507
+ * const db = await c.get(DatabaseService);
508
+ * db.query('SELECT * FROM users');
509
+ * ```
510
+ *
511
+ * @example Concurrent access (singleton behavior)
512
+ * ```typescript
513
+ * // All three calls will receive the same instance
514
+ * const [db1, db2, db3] = await Promise.all([
515
+ * c.get(DatabaseService),
516
+ * c.get(DatabaseService),
517
+ * c.get(DatabaseService)
518
+ * ]);
519
+ *
520
+ * console.log(db1 === db2 === db3); // true
521
+ * ```
522
+ *
523
+ * @example Dependency injection in factories
524
+ * ```typescript
525
+ * const c = container()
526
+ * .register(DatabaseService, () => new DatabaseService())
527
+ * .register(UserService, async (container) => {
528
+ * const db = await container.get(DatabaseService);
529
+ * return new UserService(db);
530
+ * });
531
+ *
532
+ * const userService = await c.get(UserService);
533
+ * ```
534
+ */
535
+ async get(tag) {
536
+ return resolveDependency(tag, this.cache, this.factories, this);
537
+ }
538
+ /**
539
+ * Destroys all instantiated dependencies by calling their finalizers, then clears the instance cache.
540
+ *
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.
544
+ *
545
+ * All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
546
+ * for maximum cleanup performance.
547
+ * If any finalizers fail, all errors are collected and a DependencyContainerFinalizationError
548
+ * is thrown containing details of all failures.
549
+ *
550
+ * **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
551
+ * Services should be designed to handle cleanup gracefully regardless of the order in which their
552
+ * dependencies are cleaned up.
553
+ *
554
+ * @returns Promise that resolves when all cleanup is complete
555
+ * @throws {DependencyContainerFinalizationError} If any finalizers fail during cleanup
556
+ *
557
+ * @example Basic cleanup and reuse
558
+ * ```typescript
559
+ * const c = container()
560
+ * .register(DatabaseConnection,
561
+ * async () => {
562
+ * const conn = new DatabaseConnection();
563
+ * await conn.connect();
564
+ * return conn;
565
+ * },
566
+ * (conn) => conn.disconnect() // Finalizer
567
+ * );
568
+ *
569
+ * // First use cycle
570
+ * const db1 = await c.get(DatabaseConnection);
571
+ * await c.destroy(); // Calls conn.disconnect(), clears cache
572
+ *
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
576
+ * ```
577
+ *
578
+ * @example Multiple destroy/reuse cycles
579
+ * ```typescript
580
+ * const c = container().register(UserService, () => new UserService());
581
+ *
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
+ * }
587
+ * ```
588
+ *
589
+ * @example Handling cleanup errors
590
+ * ```typescript
591
+ * try {
592
+ * await container.destroy();
593
+ * } catch (error) {
594
+ * if (error instanceof DependencyContainerFinalizationError) {
595
+ * console.error('Some dependencies failed to clean up:', error.detail.errors);
596
+ * }
597
+ * }
598
+ * // Container is still reusable even after finalizer errors
599
+ * ```
600
+ */
601
+ async destroy() {
602
+ try {
603
+ await runFinalizers(this.finalizers, this.cache);
604
+ } finally {
605
+ this.cache.clear();
606
+ }
607
+ }
608
+ };
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
+ /**
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
776
+ *
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.
779
+ *
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
+ * );
793
+ *
794
+ * // 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 });
826
+ * ```
827
+ *
828
+ * @example Complex application layer structure
829
+ * ```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
+ * );
850
+ *
851
+ * // Compose the complete application
852
+ * const appLayer = configLayer().to(infraLayer()).to(serviceLayer());
853
+ * ```
854
+ */
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
+ })();
893
+ }
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
+
906
+ //#endregion
907
+ //#region src/service.ts
908
+ /**
909
+ * Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
910
+ *
911
+ * For ClassTag services:
912
+ * - Dependencies are automatically inferred from constructor parameters
913
+ * - The factory function must handle dependency injection by resolving dependencies from the container
914
+ *
915
+ * For ValueTag services:
916
+ * - No constructor dependencies are needed since they don't have constructors
917
+ *
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
923
+ *
924
+ * @example Simple service without dependencies
925
+ * ```typescript
926
+ * class LoggerService extends Tag.Class('LoggerService') {
927
+ * log(message: string) { console.log(message); }
928
+ * }
929
+ *
930
+ * const loggerService = service(LoggerService, () => new LoggerService());
931
+ * ```
932
+ *
933
+ * @example Service with dependencies
934
+ * ```typescript
935
+ * class DatabaseService extends Tag.Class('DatabaseService') {
936
+ * query() { return []; }
937
+ * }
938
+ *
939
+ * class UserService extends Tag.Class('UserService') {
940
+ * constructor(private db: DatabaseService) {
941
+ * super();
942
+ * }
943
+ *
944
+ * getUsers() { return this.db.query(); }
945
+ * }
946
+ *
947
+ * const userService = service(UserService, async (container) =>
948
+ * new UserService(await container.get(DatabaseService))
949
+ * );
950
+ * ```
951
+ *
952
+ * @example Service with configuration parameters
953
+ * ```typescript
954
+ * class DatabaseService extends Tag.Class('DatabaseService') {
955
+ * constructor(private config: { dbUrl: string }) {
956
+ * super();
957
+ * }
958
+ * }
959
+ *
960
+ * const dbService = service(
961
+ * DatabaseService,
962
+ * (container, params: { dbUrl: string }) => new DatabaseService(params)
963
+ * );
964
+ * ```
965
+ */
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;
980
+ }
981
+
982
+ //#endregion
983
+ export { Layer, Tag, container, layer, scopedContainer, service };