sandly 0.1.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,49 +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
- Symbol("InjectSource");
5
18
  /**
6
- * Internal symbol used to identify tagged types within the dependency injection system.
7
- * 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
+ *
8
25
  * @internal
9
26
  */
10
- const TagId = "__tag_id__";
27
+ const ValueTagIdKey = "sandly/ValueTagIdKey";
28
+ const ServiceTagIdKey = "sandly/ServiceTagIdKey";
29
+ /**
30
+ * Internal string used to identify the type of a tagged type within the dependency injection system.
31
+ * This string 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.
33
+ * @internal
34
+ */
35
+ const TagTypeKey = "sandly/TagTypeKey";
11
36
  /**
12
37
  * Utility object containing factory functions for creating dependency tags.
13
38
  *
14
- * 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
15
40
  * used throughout the dependency injection system. It's the main entry point for
16
41
  * defining dependencies in a type-safe way.
17
42
  */
18
43
  const Tag = {
19
44
  of: (id) => {
20
45
  return () => ({
21
- [TagId]: id,
22
- __type: void 0
46
+ [ValueTagIdKey]: id,
47
+ [TagTypeKey]: void 0
23
48
  });
24
49
  },
25
50
  for: () => {
26
51
  return {
27
- [TagId]: Symbol(),
28
- __type: void 0
52
+ [ValueTagIdKey]: Symbol(),
53
+ [TagTypeKey]: void 0
29
54
  };
30
55
  },
31
- Class: (id) => {
56
+ Service: (id) => {
32
57
  class Tagged {
33
- static [TagId] = id;
34
- [TagId] = id;
58
+ static [ServiceTagIdKey] = id;
59
+ [ServiceTagIdKey] = id;
35
60
  }
36
61
  return Tagged;
37
62
  },
38
63
  id: (tag) => {
39
- if (typeof tag === "function") {
40
- const id$1 = tag[TagId];
41
- return typeof id$1 === "symbol" ? id$1.toString() : String(id$1);
42
- }
43
- const id = tag[TagId];
44
- 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;
45
68
  }
46
69
  };
70
+ /**
71
+ * String 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 = "sandly/InjectSource";
47
75
 
48
76
  //#endregion
49
77
  //#region src/errors.ts
@@ -86,7 +114,7 @@ var BaseError = class BaseError extends Error {
86
114
  * @example Catching DI errors
87
115
  * ```typescript
88
116
  * try {
89
- * await container.get(SomeService);
117
+ * await container.resolve(SomeService);
90
118
  * } catch (error) {
91
119
  * if (error instanceof ContainerError) {
92
120
  * console.error('DI Error:', error.message);
@@ -107,7 +135,7 @@ var DependencyAlreadyInstantiatedError = class extends ContainerError {};
107
135
  /**
108
136
  * Error thrown when attempting to use a container that has been destroyed.
109
137
  *
110
- * This error occurs when calling `container.get()`, `container.register()`, or `container.destroy()`
138
+ * This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
111
139
  * on a container that has already been destroyed. It indicates a programming error where the container
112
140
  * is being used after it has been destroyed.
113
141
  */
@@ -115,16 +143,16 @@ var ContainerDestroyedError = class extends ContainerError {};
115
143
  /**
116
144
  * Error thrown when attempting to retrieve a dependency that hasn't been registered.
117
145
  *
118
- * 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
119
147
  * registered via `container.register()`. It indicates a programming error where
120
148
  * the dependency setup is incomplete.
121
149
  *
122
150
  * @example
123
151
  * ```typescript
124
- * const c = container(); // Empty container
152
+ * const container = Container.empty(); // Empty container
125
153
  *
126
154
  * try {
127
- * await c.get(UnregisteredService); // This will throw
155
+ * await c.resolve(UnregisteredService); // This will throw
128
156
  * } catch (error) {
129
157
  * if (error instanceof UnknownDependencyError) {
130
158
  * console.error('Missing dependency:', error.message);
@@ -140,7 +168,7 @@ var UnknownDependencyError = class extends ContainerError {
140
168
  * @param tag - The dependency tag that wasn't found
141
169
  */
142
170
  constructor(tag) {
143
- super(`No factory registered for dependency ${Tag.id(tag)}`);
171
+ super(`No factory registered for dependency ${String(Tag.id(tag))}`);
144
172
  }
145
173
  };
146
174
  /**
@@ -152,19 +180,19 @@ var UnknownDependencyError = class extends ContainerError {
152
180
  *
153
181
  * @example Circular dependency scenario
154
182
  * ```typescript
155
- * class ServiceA extends Tag.Class('ServiceA') {}
156
- * class ServiceB extends Tag.Class('ServiceB') {}
183
+ * class ServiceA extends Tag.Service('ServiceA') {}
184
+ * class ServiceB extends Tag.Service('ServiceB') {}
157
185
  *
158
- * const c = container()
186
+ * const container = Container.empty()
159
187
  * .register(ServiceA, async (ctx) =>
160
- * new ServiceA(await ctx.get(ServiceB)) // Depends on B
188
+ * new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
161
189
  * )
162
190
  * .register(ServiceB, async (ctx) =>
163
- * new ServiceB(await ctx.get(ServiceA)) // Depends on A - CIRCULAR!
191
+ * new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
164
192
  * );
165
193
  *
166
194
  * try {
167
- * await c.get(ServiceA);
195
+ * await c.resolve(ServiceA);
168
196
  * } catch (error) {
169
197
  * if (error instanceof CircularDependencyError) {
170
198
  * console.error('Circular dependency:', error.message);
@@ -183,7 +211,7 @@ var CircularDependencyError = class extends ContainerError {
183
211
  */
184
212
  constructor(tag, dependencyChain) {
185
213
  const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
186
- 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: {
187
215
  tag: Tag.id(tag),
188
216
  dependencyChain: dependencyChain.map((t) => Tag.id(t))
189
217
  } });
@@ -197,14 +225,14 @@ var CircularDependencyError = class extends ContainerError {
197
225
  *
198
226
  * @example Factory throwing error
199
227
  * ```typescript
200
- * class DatabaseService extends Tag.Class('DatabaseService') {}
228
+ * class DatabaseService extends Tag.Service('DatabaseService') {}
201
229
  *
202
- * const c = container().register(DatabaseService, () => {
230
+ * const container = Container.empty().register(DatabaseService, () => {
203
231
  * throw new Error('Database connection failed');
204
232
  * });
205
233
  *
206
234
  * try {
207
- * await c.get(DatabaseService);
235
+ * await c.resolve(DatabaseService);
208
236
  * } catch (error) {
209
237
  * if (error instanceof DependencyCreationError) {
210
238
  * console.error('Failed to create:', error.message);
@@ -222,7 +250,7 @@ var DependencyCreationError = class extends ContainerError {
222
250
  * @param error - The original error thrown by the factory function
223
251
  */
224
252
  constructor(tag, error) {
225
- super(`Error creating instance of ${Tag.id(tag)}`, {
253
+ super(`Error creating instance of ${String(Tag.id(tag))}`, {
226
254
  cause: error,
227
255
  detail: { tag: Tag.id(tag) }
228
256
  });
@@ -271,6 +299,7 @@ var DependencyFinalizationError = class extends ContainerError {
271
299
  * @internal
272
300
  */
273
301
  const resolutionChain = new AsyncLocalStorage();
302
+ const ContainerTypeId = Symbol.for("sandly/Container");
274
303
  /**
275
304
  * A type-safe dependency injection container that manages service instantiation,
276
305
  * caching, and lifecycle management with support for async dependencies and
@@ -282,26 +311,26 @@ const resolutionChain = new AsyncLocalStorage();
282
311
  *
283
312
  * @template TReg - Union type of all registered dependency tags in this container
284
313
  *
285
- * @example Basic usage with class tags
314
+ * @example Basic usage with service tags
286
315
  * ```typescript
287
316
  * import { container, Tag } from 'sandly';
288
317
  *
289
- * class DatabaseService extends Tag.Class('DatabaseService') {
318
+ * class DatabaseService extends Tag.Service('DatabaseService') {
290
319
  * query() { return 'data'; }
291
320
  * }
292
321
  *
293
- * class UserService extends Tag.Class('UserService') {
322
+ * class UserService extends Tag.Service('UserService') {
294
323
  * constructor(private db: DatabaseService) {}
295
324
  * getUser() { return this.db.query(); }
296
325
  * }
297
326
  *
298
- * const c = container()
327
+ * const container = Container.empty()
299
328
  * .register(DatabaseService, () => new DatabaseService())
300
329
  * .register(UserService, async (ctx) =>
301
- * new UserService(await ctx.get(DatabaseService))
330
+ * new UserService(await ctx.resolve(DatabaseService))
302
331
  * );
303
332
  *
304
- * const userService = await c.get(UserService);
333
+ * const userService = await c.resolve(UserService);
305
334
  * ```
306
335
  *
307
336
  * @example Usage with value tags
@@ -309,22 +338,22 @@ const resolutionChain = new AsyncLocalStorage();
309
338
  * const ApiKeyTag = Tag.of('apiKey')<string>();
310
339
  * const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
311
340
  *
312
- * const c = container()
341
+ * const container = Container.empty()
313
342
  * .register(ApiKeyTag, () => process.env.API_KEY!)
314
343
  * .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
315
344
  *
316
- * const apiKey = await c.get(ApiKeyTag);
317
- * const config = await c.get(ConfigTag);
345
+ * const apiKey = await c.resolve(ApiKeyTag);
346
+ * const config = await c.resolve(ConfigTag);
318
347
  * ```
319
348
  *
320
349
  * @example With finalizers for cleanup
321
350
  * ```typescript
322
- * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
351
+ * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
323
352
  * async connect() { return; }
324
353
  * async disconnect() { return; }
325
354
  * }
326
355
  *
327
- * const c = container().register(
356
+ * const container = Container.empty().register(
328
357
  * DatabaseConnection,
329
358
  * async () => {
330
359
  * const conn = new DatabaseConnection();
@@ -339,6 +368,8 @@ const resolutionChain = new AsyncLocalStorage();
339
368
  * ```
340
369
  */
341
370
  var Container = class Container {
371
+ [ContainerTypeId];
372
+ constructor() {}
342
373
  /**
343
374
  * Cache of instantiated dependencies as promises.
344
375
  * Ensures singleton behavior and supports concurrent access.
@@ -361,11 +392,18 @@ var Container = class Container {
361
392
  */
362
393
  isDestroyed = false;
363
394
  /**
395
+ * Creates a new empty container instance.
396
+ * @returns A new empty Container instance with no registered dependencies.
397
+ */
398
+ static empty() {
399
+ return new Container();
400
+ }
401
+ /**
364
402
  * Registers a dependency in the container with a factory function and optional finalizer.
365
403
  *
366
404
  * The factory function receives the current container instance and must return the
367
405
  * service instance (or a Promise of it). The container tracks the registration at
368
- * the type level, ensuring type safety for subsequent `.get()` calls.
406
+ * the type level, ensuring type safety for subsequent `.resolve()` calls.
369
407
  *
370
408
  * If a dependency is already registered, this method will override it unless the
371
409
  * dependency has already been instantiated, in which case it will throw an error.
@@ -380,11 +418,11 @@ var Container = class Container {
380
418
  *
381
419
  * @example Registering a simple service
382
420
  * ```typescript
383
- * class LoggerService extends Tag.Class('LoggerService') {
421
+ * class LoggerService extends Tag.Service('LoggerService') {
384
422
  * log(message: string) { console.log(message); }
385
423
  * }
386
424
  *
387
- * const c = container().register(
425
+ * const container = Container.empty().register(
388
426
  * LoggerService,
389
427
  * () => new LoggerService()
390
428
  * );
@@ -392,24 +430,24 @@ var Container = class Container {
392
430
  *
393
431
  * @example Registering with dependencies
394
432
  * ```typescript
395
- * class UserService extends Tag.Class('UserService') {
433
+ * class UserService extends Tag.Service('UserService') {
396
434
  * constructor(private db: DatabaseService, private logger: LoggerService) {}
397
435
  * }
398
436
  *
399
- * const c = container()
437
+ * const container = Container.empty()
400
438
  * .register(DatabaseService, () => new DatabaseService())
401
439
  * .register(LoggerService, () => new LoggerService())
402
440
  * .register(UserService, async (ctx) =>
403
441
  * new UserService(
404
- * await ctx.get(DatabaseService),
405
- * await ctx.get(LoggerService)
442
+ * await ctx.resolve(DatabaseService),
443
+ * await ctx.resolve(LoggerService)
406
444
  * )
407
445
  * );
408
446
  * ```
409
447
  *
410
448
  * @example Overriding a dependency
411
449
  * ```typescript
412
- * const c = container()
450
+ * const container = Container.empty()
413
451
  * .register(DatabaseService, () => new DatabaseService())
414
452
  * .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
415
453
  * ```
@@ -418,7 +456,7 @@ var Container = class Container {
418
456
  * ```typescript
419
457
  * const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
420
458
  *
421
- * const c = container().register(
459
+ * const container = Container.empty().register(
422
460
  * ConfigTag,
423
461
  * () => ({ apiUrl: 'https://api.example.com' })
424
462
  * );
@@ -426,12 +464,12 @@ var Container = class Container {
426
464
  *
427
465
  * @example With finalizer for cleanup
428
466
  * ```typescript
429
- * class DatabaseConnection extends Tag.Class('DatabaseConnection') {
467
+ * class DatabaseConnection extends Tag.Service('DatabaseConnection') {
430
468
  * async connect() { return; }
431
469
  * async close() { return; }
432
470
  * }
433
471
  *
434
- * const c = container().register(
472
+ * const container = Container.empty().register(
435
473
  * DatabaseConnection,
436
474
  * async () => {
437
475
  * const conn = new DatabaseConnection();
@@ -444,7 +482,7 @@ var Container = class Container {
444
482
  */
445
483
  register(tag, spec) {
446
484
  if (this.isDestroyed) throw new ContainerDestroyedError("Cannot register dependencies on a destroyed container");
447
- if (this.has(tag) && this.exists(tag)) throw new DependencyAlreadyInstantiatedError(`Cannot register dependency ${Tag.id(tag)} - it has already been instantiated. Registration must happen before any instantiation occurs, as cached instances would still be used by existing dependencies.`);
485
+ 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.`);
448
486
  if (typeof spec === "function") {
449
487
  this.factories.set(tag, spec);
450
488
  this.finalizers.delete(tag);
@@ -465,7 +503,7 @@ var Container = class Container {
465
503
  *
466
504
  * @example
467
505
  * ```typescript
468
- * const c = container().register(DatabaseService, () => new DatabaseService());
506
+ * const container = Container.empty().register(DatabaseService, () => new DatabaseService());
469
507
  * console.log(c.has(DatabaseService)); // true
470
508
  * ```
471
509
  */
@@ -500,10 +538,10 @@ var Container = class Container {
500
538
  *
501
539
  * @example Basic usage
502
540
  * ```typescript
503
- * const c = container()
541
+ * const container = Container.empty()
504
542
  * .register(DatabaseService, () => new DatabaseService());
505
543
  *
506
- * const db = await c.get(DatabaseService);
544
+ * const db = await c.resolve(DatabaseService);
507
545
  * db.query('SELECT * FROM users');
508
546
  * ```
509
547
  *
@@ -511,9 +549,9 @@ var Container = class Container {
511
549
  * ```typescript
512
550
  * // All three calls will receive the same instance
513
551
  * const [db1, db2, db3] = await Promise.all([
514
- * c.get(DatabaseService),
515
- * c.get(DatabaseService),
516
- * c.get(DatabaseService)
552
+ * c.resolve(DatabaseService),
553
+ * c.resolve(DatabaseService),
554
+ * c.resolve(DatabaseService)
517
555
  * ]);
518
556
  *
519
557
  * console.log(db1 === db2 === db3); // true
@@ -521,17 +559,17 @@ var Container = class Container {
521
559
  *
522
560
  * @example Dependency injection in factories
523
561
  * ```typescript
524
- * const c = container()
562
+ * const container = Container.empty()
525
563
  * .register(DatabaseService, () => new DatabaseService())
526
564
  * .register(UserService, async (ctx) => {
527
- * const db = await ctx.get(DatabaseService);
565
+ * const db = await ctx.resolve(DatabaseService);
528
566
  * return new UserService(db);
529
567
  * });
530
568
  *
531
- * const userService = await c.get(UserService);
569
+ * const userService = await c.resolve(UserService);
532
570
  * ```
533
571
  */
534
- async get(tag) {
572
+ async resolve(tag) {
535
573
  if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
536
574
  const cached = this.cache.get(tag);
537
575
  if (cached !== void 0) return cached;
@@ -554,6 +592,52 @@ var Container = class Container {
554
592
  return instancePromise;
555
593
  }
556
594
  /**
595
+ * Resolves multiple dependencies concurrently using Promise.all.
596
+ *
597
+ * This method takes a variable number of dependency tags and resolves all of them concurrently,
598
+ * returning a tuple with the resolved instances in the same order as the input tags.
599
+ * The method maintains all the same guarantees as the individual resolve method:
600
+ * singleton behavior, circular dependency detection, and proper error handling.
601
+ *
602
+ * @template T - The tuple type of dependency tags to resolve
603
+ * @param tags - Variable number of dependency tags to resolve
604
+ * @returns Promise resolving to a tuple of service instances in the same order
605
+ * @throws {ContainerDestroyedError} If the container has been destroyed
606
+ * @throws {UnknownDependencyError} If any dependency is not registered
607
+ * @throws {CircularDependencyError} If a circular dependency is detected
608
+ * @throws {DependencyCreationError} If any factory function throws an error
609
+ *
610
+ * @example Basic usage
611
+ * ```typescript
612
+ * const container = Container.empty()
613
+ * .register(DatabaseService, () => new DatabaseService())
614
+ * .register(LoggerService, () => new LoggerService());
615
+ *
616
+ * const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
617
+ * ```
618
+ *
619
+ * @example Mixed tag types
620
+ * ```typescript
621
+ * const ApiKeyTag = Tag.of('apiKey')<string>();
622
+ * const container = Container.empty()
623
+ * .register(ApiKeyTag, () => 'secret-key')
624
+ * .register(UserService, () => new UserService());
625
+ *
626
+ * const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
627
+ * ```
628
+ *
629
+ * @example Empty array
630
+ * ```typescript
631
+ * const results = await c.resolveAll(); // Returns empty array
632
+ * ```
633
+ */
634
+ async resolveAll(...tags) {
635
+ if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
636
+ const promises = tags.map((tag) => this.resolve(tag));
637
+ const results = await Promise.all(promises);
638
+ return results;
639
+ }
640
+ /**
557
641
  * Copies all registrations from this container to a target container.
558
642
  *
559
643
  * @internal
@@ -587,10 +671,10 @@ var Container = class Container {
587
671
  *
588
672
  * @example Merging containers
589
673
  * ```typescript
590
- * const container1 = container()
674
+ * const container1 = Container.empty()
591
675
  * .register(DatabaseService, () => new DatabaseService());
592
676
  *
593
- * const container2 = container()
677
+ * const container2 = Container.empty()
594
678
  * .register(UserService, () => new UserService());
595
679
  *
596
680
  * const merged = container1.merge(container2);
@@ -625,7 +709,7 @@ var Container = class Container {
625
709
  *
626
710
  * @example Basic cleanup
627
711
  * ```typescript
628
- * const c = container()
712
+ * const container = Container.empty()
629
713
  * .register(DatabaseConnection,
630
714
  * async () => {
631
715
  * const conn = new DatabaseConnection();
@@ -635,12 +719,12 @@ var Container = class Container {
635
719
  * (conn) => conn.disconnect() // Finalizer
636
720
  * );
637
721
  *
638
- * const db = await c.get(DatabaseConnection);
722
+ * const db = await c.resolve(DatabaseConnection);
639
723
  * await c.destroy(); // Calls conn.disconnect(), container becomes unusable
640
724
  *
641
725
  * // This will throw an error
642
726
  * try {
643
- * await c.get(DatabaseConnection);
727
+ * await c.resolve(DatabaseConnection);
644
728
  * } catch (error) {
645
729
  * console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
646
730
  * }
@@ -648,9 +732,9 @@ var Container = class Container {
648
732
  *
649
733
  * @example Application shutdown
650
734
  * ```typescript
651
- * const appContainer = container()
735
+ * const appContainer Container.empty
652
736
  * .register(DatabaseService, () => new DatabaseService())
653
- * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.get(DatabaseService)));
737
+ * .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
654
738
  *
655
739
  * // During application shutdown
656
740
  * process.on('SIGTERM', async () => {
@@ -691,38 +775,14 @@ var Container = class Container {
691
775
  }
692
776
  }
693
777
  };
694
- /**
695
- * Creates a new empty dependency injection container.
696
- *
697
- * This is a convenience factory function that creates a new DependencyContainer instance.
698
- * The returned container starts with no registered dependencies and the type parameter
699
- * defaults to `never`, indicating no dependencies are available for retrieval yet.
700
- *
701
- * @returns A new empty DependencyContainer instance
702
- *
703
- * @example
704
- * ```typescript
705
- * import { container, Tag } from 'sandly';
706
- *
707
- * class DatabaseService extends Tag.Class('DatabaseService') {}
708
- * class UserService extends Tag.Class('UserService') {}
709
- *
710
- * const c = container()
711
- * .register(DatabaseService, () => new DatabaseService())
712
- * .register(UserService, async (ctx) =>
713
- * new UserService(await ctx.get(DatabaseService))
714
- * );
715
- *
716
- * const userService = await c.get(UserService);
717
- * ```
718
- */
719
- function container() {
720
- return new Container();
721
- }
722
778
 
723
779
  //#endregion
724
780
  //#region src/layer.ts
725
781
  /**
782
+ * The type ID for the Layer interface.
783
+ */
784
+ const LayerTypeId = Symbol.for("sandly/Layer");
785
+ /**
726
786
  * Creates a new dependency layer that encapsulates a set of dependency registrations.
727
787
  * Layers are the primary building blocks for organizing and composing dependency injection setups.
728
788
  *
@@ -736,7 +796,7 @@ function container() {
736
796
  * ```typescript
737
797
  * import { layer, Tag } from 'sandly';
738
798
  *
739
- * class DatabaseService extends Tag.Class('DatabaseService') {
799
+ * class DatabaseService extends Tag.Service('DatabaseService') {
740
800
  * constructor(private url: string = 'sqlite://memory') {}
741
801
  * query() { return 'data'; }
742
802
  * }
@@ -761,15 +821,15 @@ function container() {
761
821
  * const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
762
822
  * (container) =>
763
823
  * container
764
- * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.get(ConfigTag)))
765
- * .register(CacheService, async (ctx) => new CacheService(await ctx.get(ConfigTag)))
824
+ * .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
825
+ * .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
766
826
  * );
767
827
  *
768
828
  * // Service layer (requires infrastructure)
769
829
  * const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
770
830
  * (container) =>
771
831
  * container.register(UserService, async (ctx) =>
772
- * new UserService(await ctx.get(DatabaseService), await ctx.get(CacheService))
832
+ * new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
773
833
  * )
774
834
  * );
775
835
  *
@@ -779,7 +839,7 @@ function container() {
779
839
  */
780
840
  function layer(register) {
781
841
  const layerImpl = {
782
- register: (container$1) => register(container$1),
842
+ register: (container) => register(container),
783
843
  provide(dependency) {
784
844
  return createProvidedLayer(dependency, layerImpl);
785
845
  },
@@ -808,8 +868,8 @@ function createProvidedLayer(dependency, target) {
808
868
  * @internal
809
869
  */
810
870
  function createComposedLayer(dependency, target) {
811
- return layer((container$1) => {
812
- const containerWithDependency = dependency.register(container$1);
871
+ return layer((container) => {
872
+ const containerWithDependency = dependency.register(container);
813
873
  return target.register(containerWithDependency);
814
874
  });
815
875
  }
@@ -820,8 +880,8 @@ function createComposedLayer(dependency, target) {
820
880
  * @internal
821
881
  */
822
882
  function createMergedLayer(layer1, layer2) {
823
- return layer((container$1) => {
824
- const container1 = layer1.register(container$1);
883
+ return layer((container) => {
884
+ const container1 = layer1.register(container);
825
885
  const container2 = layer2.register(container1);
826
886
  return container2;
827
887
  });
@@ -831,7 +891,7 @@ function createMergedLayer(layer1, layer2) {
831
891
  */
832
892
  const Layer = {
833
893
  empty() {
834
- return layer((container$1) => container$1);
894
+ return layer((container) => container);
835
895
  },
836
896
  mergeAll(...layers) {
837
897
  return layers.reduce((acc, layer$1) => acc.merge(layer$1));
@@ -853,6 +913,14 @@ var ScopedContainer = class ScopedContainer extends Container {
853
913
  this.scope = scope;
854
914
  }
855
915
  /**
916
+ * Creates a new empty scoped container instance.
917
+ * @param scope - The scope identifier for this container
918
+ * @returns A new empty ScopedContainer instance with no registered dependencies
919
+ */
920
+ static empty(scope) {
921
+ return new ScopedContainer(null, scope);
922
+ }
923
+ /**
856
924
  * Registers a dependency in the scoped container.
857
925
  *
858
926
  * Overrides the base implementation to return ScopedContainer type
@@ -891,9 +959,9 @@ var ScopedContainer = class ScopedContainer extends Container {
891
959
  * 3. Otherwise, delegate to parent scope
892
960
  * 4. If no parent or parent doesn't have it, throw UnknownDependencyError
893
961
  */
894
- async get(tag) {
895
- if (this.factories.has(tag)) return super.get(tag);
896
- if (this.parent !== null) return this.parent.get(tag);
962
+ async resolve(tag) {
963
+ if (this.factories.has(tag)) return super.resolve(tag);
964
+ if (this.parent !== null) return this.parent.resolve(tag);
897
965
  throw new UnknownDependencyError(tag);
898
966
  }
899
967
  /**
@@ -973,7 +1041,7 @@ var ScopedContainer = class ScopedContainer extends Container {
973
1041
  * ```typescript
974
1042
  * import { container, scoped } from 'sandly';
975
1043
  *
976
- * const appContainer = container()
1044
+ * const appContainer = Container.empty()
977
1045
  * .register(DatabaseService, () => new DatabaseService())
978
1046
  * .register(ConfigService, () => new ConfigService());
979
1047
  *
@@ -985,10 +1053,10 @@ var ScopedContainer = class ScopedContainer extends Container {
985
1053
  *
986
1054
  * @example Copying complex registrations
987
1055
  * ```typescript
988
- * const baseContainer = container()
1056
+ * const baseContainer = Container.empty()
989
1057
  * .register(DatabaseService, () => new DatabaseService())
990
1058
  * .register(UserService, {
991
- * factory: async (ctx) => new UserService(await ctx.get(DatabaseService)),
1059
+ * factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
992
1060
  * finalizer: (service) => service.cleanup()
993
1061
  * });
994
1062
  *
@@ -996,32 +1064,31 @@ var ScopedContainer = class ScopedContainer extends Container {
996
1064
  * // scopedContainer now has all the same registrations with finalizers preserved
997
1065
  * ```
998
1066
  */
999
- function scoped(container$1, scope) {
1000
- const emptyScoped = new ScopedContainer(null, scope);
1001
- const result = emptyScoped.merge(container$1);
1002
- return result;
1067
+ function scoped(container, scope) {
1068
+ const emptyScoped = ScopedContainer.empty(scope);
1069
+ return emptyScoped.merge(container);
1003
1070
  }
1004
1071
 
1005
1072
  //#endregion
1006
1073
  //#region src/service.ts
1007
1074
  /**
1008
- * Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
1075
+ * Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
1009
1076
  *
1010
- * For ClassTag services:
1077
+ * For ServiceTag services:
1011
1078
  * - Dependencies are automatically inferred from constructor parameters
1012
1079
  * - The factory function must handle dependency injection by resolving dependencies from the container
1013
1080
  *
1014
1081
  * For ValueTag services:
1015
1082
  * - No constructor dependencies are needed since they don't have constructors
1016
1083
  *
1017
- * @template T - The tag representing the service (ClassTag or ValueTag)
1018
- * @param serviceClass - The tag (ClassTag or ValueTag)
1084
+ * @template T - The tag representing the service (ServiceTag or ValueTag)
1085
+ * @param tag - The tag (ServiceTag or ValueTag)
1019
1086
  * @param factory - Factory function for service instantiation with container
1020
1087
  * @returns The service layer
1021
1088
  *
1022
1089
  * @example Simple service without dependencies
1023
1090
  * ```typescript
1024
- * class LoggerService extends Tag.Class('LoggerService') {
1091
+ * class LoggerService extends Tag.Service('LoggerService') {
1025
1092
  * log(message: string) { console.log(message); }
1026
1093
  * }
1027
1094
  *
@@ -1030,11 +1097,11 @@ function scoped(container$1, scope) {
1030
1097
  *
1031
1098
  * @example Service with dependencies
1032
1099
  * ```typescript
1033
- * class DatabaseService extends Tag.Class('DatabaseService') {
1100
+ * class DatabaseService extends Tag.Service('DatabaseService') {
1034
1101
  * query() { return []; }
1035
1102
  * }
1036
1103
  *
1037
- * class UserService extends Tag.Class('UserService') {
1104
+ * class UserService extends Tag.Service('UserService') {
1038
1105
  * constructor(private db: DatabaseService) {
1039
1106
  * super();
1040
1107
  * }
@@ -1043,15 +1110,131 @@ function scoped(container$1, scope) {
1043
1110
  * }
1044
1111
  *
1045
1112
  * const userService = service(UserService, async (ctx) =>
1046
- * new UserService(await ctx.get(DatabaseService))
1113
+ * new UserService(await ctx.resolve(DatabaseService))
1047
1114
  * );
1048
1115
  * ```
1049
1116
  */
1050
- function service(serviceClass, spec) {
1051
- return layer((container$1) => {
1052
- return container$1.register(serviceClass, spec);
1117
+ function service(tag, spec) {
1118
+ return layer((container) => {
1119
+ return container.register(tag, spec);
1053
1120
  });
1054
1121
  }
1122
+ /**
1123
+ * Creates a service layer with automatic dependency injection by inferring constructor parameters.
1124
+ *
1125
+ * This is a convenience function that automatically resolves constructor dependencies and passes
1126
+ * both DI-managed dependencies and static values to the service constructor in the correct order.
1127
+ * It eliminates the need to manually write factory functions for services with constructor dependencies.
1128
+ *
1129
+ * @template T - The ServiceTag representing the service class
1130
+ * @param tag - The service tag (must be a ServiceTag, not a ValueTag)
1131
+ * @param deps - Tuple of constructor parameters in order - mix of dependency tags and static values
1132
+ * @param finalizer - Optional cleanup function called when the container is destroyed
1133
+ * @returns A service layer that automatically handles dependency injection
1134
+ *
1135
+ * @example Simple service with dependencies
1136
+ * ```typescript
1137
+ * class DatabaseService extends Tag.Service('DatabaseService') {
1138
+ * constructor(private url: string) {
1139
+ * super();
1140
+ * }
1141
+ * connect() { return `Connected to ${this.url}`; }
1142
+ * }
1143
+ *
1144
+ * class UserService extends Tag.Service('UserService') {
1145
+ * constructor(private db: DatabaseService, private timeout: number) {
1146
+ * super();
1147
+ * }
1148
+ * getUsers() { return this.db.query('SELECT * FROM users'); }
1149
+ * }
1150
+ *
1151
+ * // Automatically inject DatabaseService and pass static timeout value
1152
+ * const userService = autoService(UserService, [DatabaseService, 5000]);
1153
+ * ```
1154
+ *
1155
+ * @example Mixed dependencies and static values
1156
+ * ```typescript
1157
+ * class NotificationService extends Tag.Service('NotificationService') {
1158
+ * constructor(
1159
+ * private logger: LoggerService,
1160
+ * private apiKey: string,
1161
+ * private retries: number,
1162
+ * private cache: CacheService
1163
+ * ) {
1164
+ * super();
1165
+ * }
1166
+ * }
1167
+ *
1168
+ * // Mix of DI tags and static values in constructor order
1169
+ * const notificationService = autoService(NotificationService, [
1170
+ * LoggerService, // Will be resolved from container
1171
+ * 'secret-api-key', // Static string value
1172
+ * 3, // Static number value
1173
+ * CacheService // Will be resolved from container
1174
+ * ]);
1175
+ * ```
1176
+ *
1177
+ * @example Compared to manual service creation
1178
+ * ```typescript
1179
+ * // Manual approach (more verbose)
1180
+ * const userServiceManual = service(UserService, async (ctx) => {
1181
+ * const db = await ctx.resolve(DatabaseService);
1182
+ * return new UserService(db, 5000);
1183
+ * });
1184
+ *
1185
+ * // Auto approach (concise)
1186
+ * const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
1187
+ * ```
1188
+ *
1189
+ * @example With finalizer for cleanup
1190
+ * ```typescript
1191
+ * class DatabaseService extends Tag.Service('DatabaseService') {
1192
+ * constructor(private connectionString: string) {
1193
+ * super();
1194
+ * }
1195
+ *
1196
+ * private connection: Connection | null = null;
1197
+ *
1198
+ * async connect() {
1199
+ * this.connection = await createConnection(this.connectionString);
1200
+ * }
1201
+ *
1202
+ * async disconnect() {
1203
+ * if (this.connection) {
1204
+ * await this.connection.close();
1205
+ * this.connection = null;
1206
+ * }
1207
+ * }
1208
+ * }
1209
+ *
1210
+ * // Service with automatic cleanup
1211
+ * const dbService = autoService(
1212
+ * DatabaseService,
1213
+ * {
1214
+ * dependencies: ['postgresql://localhost:5432/mydb'],
1215
+ * finalizer: (service) => service.disconnect() // Finalizer for cleanup
1216
+ * }
1217
+ * );
1218
+ * ```
1219
+ */
1220
+ function autoService(tag, spec) {
1221
+ if (Array.isArray(spec)) spec = { dependencies: spec };
1222
+ const factory = async (ctx) => {
1223
+ const diDeps = [];
1224
+ for (const dep of spec.dependencies) if (Tag.isTag(dep)) diDeps.push(dep);
1225
+ const resolved = await ctx.resolveAll(...diDeps);
1226
+ const args = [];
1227
+ let resolvedIndex = 0;
1228
+ for (const dep of spec.dependencies) if (Tag.isTag(dep)) args.push(resolved[resolvedIndex++]);
1229
+ else args.push(dep);
1230
+ return new tag(...args);
1231
+ };
1232
+ const finalSpec = spec.finalizer ? {
1233
+ factory,
1234
+ finalizer: spec.finalizer
1235
+ } : factory;
1236
+ return service(tag, finalSpec);
1237
+ }
1055
1238
 
1056
1239
  //#endregion
1057
1240
  //#region src/value.ts
@@ -1074,8 +1257,8 @@ function service(serviceClass, spec) {
1074
1257
  * ```
1075
1258
  */
1076
1259
  function value(tag, constantValue) {
1077
- return layer((container$1) => container$1.register(tag, () => constantValue));
1260
+ return layer((container) => container.register(tag, () => constantValue));
1078
1261
  }
1079
1262
 
1080
1263
  //#endregion
1081
- export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, Layer, ScopedContainer, Tag, UnknownDependencyError, container, layer, scoped, service, value };
1264
+ export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, InjectSource, Layer, ScopedContainer, Tag, UnknownDependencyError, autoService, layer, scoped, service, value };