sandly 0.1.0 → 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.
- package/README.md +19 -16
- package/dist/index.d.ts +371 -226
- package/dist/index.js +193 -136
- package/package.json +1 -1
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
|
-
*
|
|
7
|
-
* This symbol is used as a property key to attach metadata to both value tags and
|
|
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.
|
|
8
33
|
* @internal
|
|
9
34
|
*/
|
|
10
|
-
const
|
|
35
|
+
const TagTypeKey = Symbol.for("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
|
|
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
|
-
[
|
|
22
|
-
|
|
46
|
+
[ValueTagIdKey]: id,
|
|
47
|
+
[TagTypeKey]: void 0
|
|
23
48
|
});
|
|
24
49
|
},
|
|
25
50
|
for: () => {
|
|
26
51
|
return {
|
|
27
|
-
[
|
|
28
|
-
|
|
52
|
+
[ValueTagIdKey]: Symbol(),
|
|
53
|
+
[TagTypeKey]: void 0
|
|
29
54
|
};
|
|
30
55
|
},
|
|
31
|
-
|
|
56
|
+
Service: (id) => {
|
|
32
57
|
class Tagged {
|
|
33
|
-
static [
|
|
34
|
-
[
|
|
58
|
+
static [ServiceTagIdKey] = id;
|
|
59
|
+
[ServiceTagIdKey] = id;
|
|
35
60
|
}
|
|
36
61
|
return Tagged;
|
|
37
62
|
},
|
|
38
63
|
id: (tag) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
* 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");
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
152
|
+
* const c = Container.empty(); // Empty container
|
|
125
153
|
*
|
|
126
154
|
* try {
|
|
127
|
-
* await c.
|
|
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.
|
|
156
|
-
* class ServiceB extends Tag.
|
|
183
|
+
* class ServiceA extends Tag.Service('ServiceA') {}
|
|
184
|
+
* class ServiceB extends Tag.Service('ServiceB') {}
|
|
157
185
|
*
|
|
158
|
-
* const c =
|
|
186
|
+
* const c = Container.empty()
|
|
159
187
|
* .register(ServiceA, async (ctx) =>
|
|
160
|
-
* new ServiceA(await ctx.
|
|
188
|
+
* new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
|
|
161
189
|
* )
|
|
162
190
|
* .register(ServiceB, async (ctx) =>
|
|
163
|
-
* new ServiceB(await ctx.
|
|
191
|
+
* new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
|
|
164
192
|
* );
|
|
165
193
|
*
|
|
166
194
|
* try {
|
|
167
|
-
* await c.
|
|
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.
|
|
228
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
201
229
|
*
|
|
202
|
-
* const c =
|
|
230
|
+
* const c = Container.empty().register(DatabaseService, () => {
|
|
203
231
|
* throw new Error('Database connection failed');
|
|
204
232
|
* });
|
|
205
233
|
*
|
|
206
234
|
* try {
|
|
207
|
-
* await c.
|
|
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
|
|
314
|
+
* @example Basic usage with service tags
|
|
286
315
|
* ```typescript
|
|
287
316
|
* import { container, Tag } from 'sandly';
|
|
288
317
|
*
|
|
289
|
-
* class DatabaseService extends Tag.
|
|
318
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
290
319
|
* query() { return 'data'; }
|
|
291
320
|
* }
|
|
292
321
|
*
|
|
293
|
-
* class UserService extends Tag.
|
|
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 =
|
|
327
|
+
* const c = Container.empty()
|
|
299
328
|
* .register(DatabaseService, () => new DatabaseService())
|
|
300
329
|
* .register(UserService, async (ctx) =>
|
|
301
|
-
* new UserService(await ctx.
|
|
330
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
302
331
|
* );
|
|
303
332
|
*
|
|
304
|
-
* const userService = await c.
|
|
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 =
|
|
341
|
+
* const c = 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.
|
|
317
|
-
* const config = await c.
|
|
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.
|
|
351
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
323
352
|
* async connect() { return; }
|
|
324
353
|
* async disconnect() { return; }
|
|
325
354
|
* }
|
|
326
355
|
*
|
|
327
|
-
* const c =
|
|
356
|
+
* const c = Container.empty().register(
|
|
328
357
|
* DatabaseConnection,
|
|
329
358
|
* async () => {
|
|
330
359
|
* const conn = new DatabaseConnection();
|
|
@@ -339,6 +368,7 @@ const resolutionChain = new AsyncLocalStorage();
|
|
|
339
368
|
* ```
|
|
340
369
|
*/
|
|
341
370
|
var Container = class Container {
|
|
371
|
+
[ContainerTypeId];
|
|
342
372
|
/**
|
|
343
373
|
* Cache of instantiated dependencies as promises.
|
|
344
374
|
* Ensures singleton behavior and supports concurrent access.
|
|
@@ -360,12 +390,15 @@ var Container = class Container {
|
|
|
360
390
|
* @internal
|
|
361
391
|
*/
|
|
362
392
|
isDestroyed = false;
|
|
393
|
+
static empty() {
|
|
394
|
+
return new Container();
|
|
395
|
+
}
|
|
363
396
|
/**
|
|
364
397
|
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
365
398
|
*
|
|
366
399
|
* The factory function receives the current container instance and must return the
|
|
367
400
|
* service instance (or a Promise of it). The container tracks the registration at
|
|
368
|
-
* the type level, ensuring type safety for subsequent `.
|
|
401
|
+
* the type level, ensuring type safety for subsequent `.resolve()` calls.
|
|
369
402
|
*
|
|
370
403
|
* If a dependency is already registered, this method will override it unless the
|
|
371
404
|
* dependency has already been instantiated, in which case it will throw an error.
|
|
@@ -380,11 +413,11 @@ var Container = class Container {
|
|
|
380
413
|
*
|
|
381
414
|
* @example Registering a simple service
|
|
382
415
|
* ```typescript
|
|
383
|
-
* class LoggerService extends Tag.
|
|
416
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
384
417
|
* log(message: string) { console.log(message); }
|
|
385
418
|
* }
|
|
386
419
|
*
|
|
387
|
-
* const c =
|
|
420
|
+
* const c = Container.empty().register(
|
|
388
421
|
* LoggerService,
|
|
389
422
|
* () => new LoggerService()
|
|
390
423
|
* );
|
|
@@ -392,24 +425,24 @@ var Container = class Container {
|
|
|
392
425
|
*
|
|
393
426
|
* @example Registering with dependencies
|
|
394
427
|
* ```typescript
|
|
395
|
-
* class UserService extends Tag.
|
|
428
|
+
* class UserService extends Tag.Service('UserService') {
|
|
396
429
|
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
397
430
|
* }
|
|
398
431
|
*
|
|
399
|
-
* const c =
|
|
432
|
+
* const c = Container.empty()
|
|
400
433
|
* .register(DatabaseService, () => new DatabaseService())
|
|
401
434
|
* .register(LoggerService, () => new LoggerService())
|
|
402
435
|
* .register(UserService, async (ctx) =>
|
|
403
436
|
* new UserService(
|
|
404
|
-
* await ctx.
|
|
405
|
-
* await ctx.
|
|
437
|
+
* await ctx.resolve(DatabaseService),
|
|
438
|
+
* await ctx.resolve(LoggerService)
|
|
406
439
|
* )
|
|
407
440
|
* );
|
|
408
441
|
* ```
|
|
409
442
|
*
|
|
410
443
|
* @example Overriding a dependency
|
|
411
444
|
* ```typescript
|
|
412
|
-
* const c =
|
|
445
|
+
* const c = Container.empty()
|
|
413
446
|
* .register(DatabaseService, () => new DatabaseService())
|
|
414
447
|
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
415
448
|
* ```
|
|
@@ -418,7 +451,7 @@ var Container = class Container {
|
|
|
418
451
|
* ```typescript
|
|
419
452
|
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
420
453
|
*
|
|
421
|
-
* const c =
|
|
454
|
+
* const c = Container.empty().register(
|
|
422
455
|
* ConfigTag,
|
|
423
456
|
* () => ({ apiUrl: 'https://api.example.com' })
|
|
424
457
|
* );
|
|
@@ -426,12 +459,12 @@ var Container = class Container {
|
|
|
426
459
|
*
|
|
427
460
|
* @example With finalizer for cleanup
|
|
428
461
|
* ```typescript
|
|
429
|
-
* class DatabaseConnection extends Tag.
|
|
462
|
+
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
430
463
|
* async connect() { return; }
|
|
431
464
|
* async close() { return; }
|
|
432
465
|
* }
|
|
433
466
|
*
|
|
434
|
-
* const c =
|
|
467
|
+
* const c = Container.empty().register(
|
|
435
468
|
* DatabaseConnection,
|
|
436
469
|
* async () => {
|
|
437
470
|
* const conn = new DatabaseConnection();
|
|
@@ -444,7 +477,7 @@ var Container = class Container {
|
|
|
444
477
|
*/
|
|
445
478
|
register(tag, spec) {
|
|
446
479
|
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.`);
|
|
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.`);
|
|
448
481
|
if (typeof spec === "function") {
|
|
449
482
|
this.factories.set(tag, spec);
|
|
450
483
|
this.finalizers.delete(tag);
|
|
@@ -465,7 +498,7 @@ var Container = class Container {
|
|
|
465
498
|
*
|
|
466
499
|
* @example
|
|
467
500
|
* ```typescript
|
|
468
|
-
* const c =
|
|
501
|
+
* const c = Container.empty().register(DatabaseService, () => new DatabaseService());
|
|
469
502
|
* console.log(c.has(DatabaseService)); // true
|
|
470
503
|
* ```
|
|
471
504
|
*/
|
|
@@ -500,10 +533,10 @@ var Container = class Container {
|
|
|
500
533
|
*
|
|
501
534
|
* @example Basic usage
|
|
502
535
|
* ```typescript
|
|
503
|
-
* const c =
|
|
536
|
+
* const c = Container.empty()
|
|
504
537
|
* .register(DatabaseService, () => new DatabaseService());
|
|
505
538
|
*
|
|
506
|
-
* const db = await c.
|
|
539
|
+
* const db = await c.resolve(DatabaseService);
|
|
507
540
|
* db.query('SELECT * FROM users');
|
|
508
541
|
* ```
|
|
509
542
|
*
|
|
@@ -511,9 +544,9 @@ var Container = class Container {
|
|
|
511
544
|
* ```typescript
|
|
512
545
|
* // All three calls will receive the same instance
|
|
513
546
|
* const [db1, db2, db3] = await Promise.all([
|
|
514
|
-
* c.
|
|
515
|
-
* c.
|
|
516
|
-
* c.
|
|
547
|
+
* c.resolve(DatabaseService),
|
|
548
|
+
* c.resolve(DatabaseService),
|
|
549
|
+
* c.resolve(DatabaseService)
|
|
517
550
|
* ]);
|
|
518
551
|
*
|
|
519
552
|
* console.log(db1 === db2 === db3); // true
|
|
@@ -521,17 +554,17 @@ var Container = class Container {
|
|
|
521
554
|
*
|
|
522
555
|
* @example Dependency injection in factories
|
|
523
556
|
* ```typescript
|
|
524
|
-
* const c =
|
|
557
|
+
* const c = Container.empty()
|
|
525
558
|
* .register(DatabaseService, () => new DatabaseService())
|
|
526
559
|
* .register(UserService, async (ctx) => {
|
|
527
|
-
* const db = await ctx.
|
|
560
|
+
* const db = await ctx.resolve(DatabaseService);
|
|
528
561
|
* return new UserService(db);
|
|
529
562
|
* });
|
|
530
563
|
*
|
|
531
|
-
* const userService = await c.
|
|
564
|
+
* const userService = await c.resolve(UserService);
|
|
532
565
|
* ```
|
|
533
566
|
*/
|
|
534
|
-
async
|
|
567
|
+
async resolve(tag) {
|
|
535
568
|
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
|
|
536
569
|
const cached = this.cache.get(tag);
|
|
537
570
|
if (cached !== void 0) return cached;
|
|
@@ -554,6 +587,52 @@ var Container = class Container {
|
|
|
554
587
|
return instancePromise;
|
|
555
588
|
}
|
|
556
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
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
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
|
+
/**
|
|
557
636
|
* Copies all registrations from this container to a target container.
|
|
558
637
|
*
|
|
559
638
|
* @internal
|
|
@@ -587,10 +666,10 @@ var Container = class Container {
|
|
|
587
666
|
*
|
|
588
667
|
* @example Merging containers
|
|
589
668
|
* ```typescript
|
|
590
|
-
* const container1 =
|
|
669
|
+
* const container1 = Container.empty()
|
|
591
670
|
* .register(DatabaseService, () => new DatabaseService());
|
|
592
671
|
*
|
|
593
|
-
* const container2 =
|
|
672
|
+
* const container2 = Container.empty()
|
|
594
673
|
* .register(UserService, () => new UserService());
|
|
595
674
|
*
|
|
596
675
|
* const merged = container1.merge(container2);
|
|
@@ -625,7 +704,7 @@ var Container = class Container {
|
|
|
625
704
|
*
|
|
626
705
|
* @example Basic cleanup
|
|
627
706
|
* ```typescript
|
|
628
|
-
* const c =
|
|
707
|
+
* const c = Container.empty()
|
|
629
708
|
* .register(DatabaseConnection,
|
|
630
709
|
* async () => {
|
|
631
710
|
* const conn = new DatabaseConnection();
|
|
@@ -635,12 +714,12 @@ var Container = class Container {
|
|
|
635
714
|
* (conn) => conn.disconnect() // Finalizer
|
|
636
715
|
* );
|
|
637
716
|
*
|
|
638
|
-
* const db = await c.
|
|
717
|
+
* const db = await c.resolve(DatabaseConnection);
|
|
639
718
|
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
640
719
|
*
|
|
641
720
|
* // This will throw an error
|
|
642
721
|
* try {
|
|
643
|
-
* await c.
|
|
722
|
+
* await c.resolve(DatabaseConnection);
|
|
644
723
|
* } catch (error) {
|
|
645
724
|
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
646
725
|
* }
|
|
@@ -648,9 +727,9 @@ var Container = class Container {
|
|
|
648
727
|
*
|
|
649
728
|
* @example Application shutdown
|
|
650
729
|
* ```typescript
|
|
651
|
-
* const appContainer
|
|
730
|
+
* const appContainer Container.empty
|
|
652
731
|
* .register(DatabaseService, () => new DatabaseService())
|
|
653
|
-
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.
|
|
732
|
+
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
|
|
654
733
|
*
|
|
655
734
|
* // During application shutdown
|
|
656
735
|
* process.on('SIGTERM', async () => {
|
|
@@ -691,38 +770,14 @@ var Container = class Container {
|
|
|
691
770
|
}
|
|
692
771
|
}
|
|
693
772
|
};
|
|
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
773
|
|
|
723
774
|
//#endregion
|
|
724
775
|
//#region src/layer.ts
|
|
725
776
|
/**
|
|
777
|
+
* The type ID for the Layer interface.
|
|
778
|
+
*/
|
|
779
|
+
const LayerTypeId = Symbol.for("sandly/Layer");
|
|
780
|
+
/**
|
|
726
781
|
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
727
782
|
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
728
783
|
*
|
|
@@ -736,7 +791,7 @@ function container() {
|
|
|
736
791
|
* ```typescript
|
|
737
792
|
* import { layer, Tag } from 'sandly';
|
|
738
793
|
*
|
|
739
|
-
* class DatabaseService extends Tag.
|
|
794
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
740
795
|
* constructor(private url: string = 'sqlite://memory') {}
|
|
741
796
|
* query() { return 'data'; }
|
|
742
797
|
* }
|
|
@@ -761,15 +816,15 @@ function container() {
|
|
|
761
816
|
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
762
817
|
* (container) =>
|
|
763
818
|
* container
|
|
764
|
-
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.
|
|
765
|
-
* .register(CacheService, async (ctx) => new CacheService(await ctx.
|
|
819
|
+
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
|
|
820
|
+
* .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
|
|
766
821
|
* );
|
|
767
822
|
*
|
|
768
823
|
* // Service layer (requires infrastructure)
|
|
769
824
|
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
770
825
|
* (container) =>
|
|
771
826
|
* container.register(UserService, async (ctx) =>
|
|
772
|
-
* new UserService(await ctx.
|
|
827
|
+
* new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
|
|
773
828
|
* )
|
|
774
829
|
* );
|
|
775
830
|
*
|
|
@@ -779,7 +834,7 @@ function container() {
|
|
|
779
834
|
*/
|
|
780
835
|
function layer(register) {
|
|
781
836
|
const layerImpl = {
|
|
782
|
-
register: (container
|
|
837
|
+
register: (container) => register(container),
|
|
783
838
|
provide(dependency) {
|
|
784
839
|
return createProvidedLayer(dependency, layerImpl);
|
|
785
840
|
},
|
|
@@ -808,8 +863,8 @@ function createProvidedLayer(dependency, target) {
|
|
|
808
863
|
* @internal
|
|
809
864
|
*/
|
|
810
865
|
function createComposedLayer(dependency, target) {
|
|
811
|
-
return layer((container
|
|
812
|
-
const containerWithDependency = dependency.register(container
|
|
866
|
+
return layer((container) => {
|
|
867
|
+
const containerWithDependency = dependency.register(container);
|
|
813
868
|
return target.register(containerWithDependency);
|
|
814
869
|
});
|
|
815
870
|
}
|
|
@@ -820,8 +875,8 @@ function createComposedLayer(dependency, target) {
|
|
|
820
875
|
* @internal
|
|
821
876
|
*/
|
|
822
877
|
function createMergedLayer(layer1, layer2) {
|
|
823
|
-
return layer((container
|
|
824
|
-
const container1 = layer1.register(container
|
|
878
|
+
return layer((container) => {
|
|
879
|
+
const container1 = layer1.register(container);
|
|
825
880
|
const container2 = layer2.register(container1);
|
|
826
881
|
return container2;
|
|
827
882
|
});
|
|
@@ -831,7 +886,7 @@ function createMergedLayer(layer1, layer2) {
|
|
|
831
886
|
*/
|
|
832
887
|
const Layer = {
|
|
833
888
|
empty() {
|
|
834
|
-
return layer((container
|
|
889
|
+
return layer((container) => container);
|
|
835
890
|
},
|
|
836
891
|
mergeAll(...layers) {
|
|
837
892
|
return layers.reduce((acc, layer$1) => acc.merge(layer$1));
|
|
@@ -852,6 +907,9 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
852
907
|
this.parent = parent;
|
|
853
908
|
this.scope = scope;
|
|
854
909
|
}
|
|
910
|
+
static empty(scope) {
|
|
911
|
+
return new ScopedContainer(null, scope);
|
|
912
|
+
}
|
|
855
913
|
/**
|
|
856
914
|
* Registers a dependency in the scoped container.
|
|
857
915
|
*
|
|
@@ -891,9 +949,9 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
891
949
|
* 3. Otherwise, delegate to parent scope
|
|
892
950
|
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
893
951
|
*/
|
|
894
|
-
async
|
|
895
|
-
if (this.factories.has(tag)) return super.
|
|
896
|
-
if (this.parent !== null) return this.parent.
|
|
952
|
+
async resolve(tag) {
|
|
953
|
+
if (this.factories.has(tag)) return super.resolve(tag);
|
|
954
|
+
if (this.parent !== null) return this.parent.resolve(tag);
|
|
897
955
|
throw new UnknownDependencyError(tag);
|
|
898
956
|
}
|
|
899
957
|
/**
|
|
@@ -973,7 +1031,7 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
973
1031
|
* ```typescript
|
|
974
1032
|
* import { container, scoped } from 'sandly';
|
|
975
1033
|
*
|
|
976
|
-
* const appContainer =
|
|
1034
|
+
* const appContainer = Container.empty()
|
|
977
1035
|
* .register(DatabaseService, () => new DatabaseService())
|
|
978
1036
|
* .register(ConfigService, () => new ConfigService());
|
|
979
1037
|
*
|
|
@@ -985,10 +1043,10 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
985
1043
|
*
|
|
986
1044
|
* @example Copying complex registrations
|
|
987
1045
|
* ```typescript
|
|
988
|
-
* const baseContainer =
|
|
1046
|
+
* const baseContainer = Container.empty()
|
|
989
1047
|
* .register(DatabaseService, () => new DatabaseService())
|
|
990
1048
|
* .register(UserService, {
|
|
991
|
-
* factory: async (ctx) => new UserService(await ctx.
|
|
1049
|
+
* factory: async (ctx) => new UserService(await ctx.resolve(DatabaseService)),
|
|
992
1050
|
* finalizer: (service) => service.cleanup()
|
|
993
1051
|
* });
|
|
994
1052
|
*
|
|
@@ -996,32 +1054,31 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
996
1054
|
* // scopedContainer now has all the same registrations with finalizers preserved
|
|
997
1055
|
* ```
|
|
998
1056
|
*/
|
|
999
|
-
function scoped(container
|
|
1000
|
-
const emptyScoped =
|
|
1001
|
-
|
|
1002
|
-
return result;
|
|
1057
|
+
function scoped(container, scope) {
|
|
1058
|
+
const emptyScoped = ScopedContainer.empty(scope);
|
|
1059
|
+
return emptyScoped.merge(container);
|
|
1003
1060
|
}
|
|
1004
1061
|
|
|
1005
1062
|
//#endregion
|
|
1006
1063
|
//#region src/service.ts
|
|
1007
1064
|
/**
|
|
1008
|
-
* Creates a service layer from any tag type (
|
|
1065
|
+
* Creates a service layer from any tag type (ServiceTag or ValueTag) with optional parameters.
|
|
1009
1066
|
*
|
|
1010
|
-
* For
|
|
1067
|
+
* For ServiceTag services:
|
|
1011
1068
|
* - Dependencies are automatically inferred from constructor parameters
|
|
1012
1069
|
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
1013
1070
|
*
|
|
1014
1071
|
* For ValueTag services:
|
|
1015
1072
|
* - No constructor dependencies are needed since they don't have constructors
|
|
1016
1073
|
*
|
|
1017
|
-
* @template T - The tag representing the service (
|
|
1018
|
-
* @param
|
|
1074
|
+
* @template T - The tag representing the service (ServiceTag or ValueTag)
|
|
1075
|
+
* @param tag - The tag (ServiceTag or ValueTag)
|
|
1019
1076
|
* @param factory - Factory function for service instantiation with container
|
|
1020
1077
|
* @returns The service layer
|
|
1021
1078
|
*
|
|
1022
1079
|
* @example Simple service without dependencies
|
|
1023
1080
|
* ```typescript
|
|
1024
|
-
* class LoggerService extends Tag.
|
|
1081
|
+
* class LoggerService extends Tag.Service('LoggerService') {
|
|
1025
1082
|
* log(message: string) { console.log(message); }
|
|
1026
1083
|
* }
|
|
1027
1084
|
*
|
|
@@ -1030,11 +1087,11 @@ function scoped(container$1, scope) {
|
|
|
1030
1087
|
*
|
|
1031
1088
|
* @example Service with dependencies
|
|
1032
1089
|
* ```typescript
|
|
1033
|
-
* class DatabaseService extends Tag.
|
|
1090
|
+
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1034
1091
|
* query() { return []; }
|
|
1035
1092
|
* }
|
|
1036
1093
|
*
|
|
1037
|
-
* class UserService extends Tag.
|
|
1094
|
+
* class UserService extends Tag.Service('UserService') {
|
|
1038
1095
|
* constructor(private db: DatabaseService) {
|
|
1039
1096
|
* super();
|
|
1040
1097
|
* }
|
|
@@ -1043,13 +1100,13 @@ function scoped(container$1, scope) {
|
|
|
1043
1100
|
* }
|
|
1044
1101
|
*
|
|
1045
1102
|
* const userService = service(UserService, async (ctx) =>
|
|
1046
|
-
* new UserService(await ctx.
|
|
1103
|
+
* new UserService(await ctx.resolve(DatabaseService))
|
|
1047
1104
|
* );
|
|
1048
1105
|
* ```
|
|
1049
1106
|
*/
|
|
1050
|
-
function service(
|
|
1051
|
-
return layer((container
|
|
1052
|
-
return container
|
|
1107
|
+
function service(tag, spec) {
|
|
1108
|
+
return layer((container) => {
|
|
1109
|
+
return container.register(tag, spec);
|
|
1053
1110
|
});
|
|
1054
1111
|
}
|
|
1055
1112
|
|
|
@@ -1074,8 +1131,8 @@ function service(serviceClass, spec) {
|
|
|
1074
1131
|
* ```
|
|
1075
1132
|
*/
|
|
1076
1133
|
function value(tag, constantValue) {
|
|
1077
|
-
return layer((container
|
|
1134
|
+
return layer((container) => container.register(tag, () => constantValue));
|
|
1078
1135
|
}
|
|
1079
1136
|
|
|
1080
1137
|
//#endregion
|
|
1081
|
-
export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, Layer, ScopedContainer, Tag, UnknownDependencyError,
|
|
1138
|
+
export { CircularDependencyError, Container, ContainerDestroyedError, ContainerError, DependencyAlreadyInstantiatedError, DependencyCreationError, DependencyFinalizationError, InjectSource, Layer, ScopedContainer, Tag, UnknownDependencyError, layer, scoped, service, value };
|