sandly 1.0.1 → 2.0.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 +314 -2624
- package/dist/index.d.ts +645 -1557
- package/dist/index.js +522 -921
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,197 +1,24 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
/**
|
|
3
|
-
* The type ID for the Layer interface.
|
|
4
|
-
*/
|
|
5
|
-
const LayerTypeId = Symbol.for("sandly/Layer");
|
|
6
|
-
/**
|
|
7
|
-
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
8
|
-
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
9
|
-
*
|
|
10
|
-
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
11
|
-
* @template TProvides - The union of dependency tags this layer registers/provides
|
|
12
|
-
*
|
|
13
|
-
* @param register - Function that performs the dependency registrations. Receives a container.
|
|
14
|
-
* @returns The layer instance.
|
|
15
|
-
*
|
|
16
|
-
* @example Simple layer
|
|
17
|
-
* ```typescript
|
|
18
|
-
* import { layer, Tag } from 'sandly';
|
|
19
|
-
*
|
|
20
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
21
|
-
* constructor(private url: string = 'sqlite://memory') {}
|
|
22
|
-
* query() { return 'data'; }
|
|
23
|
-
* }
|
|
24
|
-
*
|
|
25
|
-
* // Layer that provides DatabaseService, requires nothing
|
|
26
|
-
* const databaseLayer = layer<never, typeof DatabaseService>((container) =>
|
|
27
|
-
* container.register(DatabaseService, () => new DatabaseService())
|
|
28
|
-
* );
|
|
29
|
-
*
|
|
30
|
-
* // Usage
|
|
31
|
-
* const dbLayerInstance = databaseLayer;
|
|
32
|
-
* ```
|
|
33
|
-
*
|
|
34
|
-
* @example Complex application layer structure
|
|
35
|
-
* ```typescript
|
|
36
|
-
* // Configuration layer
|
|
37
|
-
* const configLayer = layer<never, typeof ConfigTag>((container) =>
|
|
38
|
-
* container.register(ConfigTag, () => loadConfig())
|
|
39
|
-
* );
|
|
40
|
-
*
|
|
41
|
-
* // Infrastructure layer (requires config)
|
|
42
|
-
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
43
|
-
* (container) =>
|
|
44
|
-
* container
|
|
45
|
-
* .register(DatabaseService, async (ctx) => new DatabaseService(await ctx.resolve(ConfigTag)))
|
|
46
|
-
* .register(CacheService, async (ctx) => new CacheService(await ctx.resolve(ConfigTag)))
|
|
47
|
-
* );
|
|
48
|
-
*
|
|
49
|
-
* // Service layer (requires infrastructure)
|
|
50
|
-
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
51
|
-
* (container) =>
|
|
52
|
-
* container.register(UserService, async (ctx) =>
|
|
53
|
-
* new UserService(await ctx.resolve(DatabaseService), await ctx.resolve(CacheService))
|
|
54
|
-
* )
|
|
55
|
-
* );
|
|
56
|
-
*
|
|
57
|
-
* // Compose the complete application
|
|
58
|
-
* const appLayer = serviceLayer.provide(infraLayer).provide(configLayer);
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
function layer(register) {
|
|
62
|
-
const layerImpl = {
|
|
63
|
-
register: (container) => register(container),
|
|
64
|
-
provide(dependency$1) {
|
|
65
|
-
return createProvidedLayer(dependency$1, layerImpl);
|
|
66
|
-
},
|
|
67
|
-
provideMerge(dependency$1) {
|
|
68
|
-
return createComposedLayer(dependency$1, layerImpl);
|
|
69
|
-
},
|
|
70
|
-
merge(other) {
|
|
71
|
-
return createMergedLayer(layerImpl, other);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
return layerImpl;
|
|
75
|
-
}
|
|
1
|
+
//#region src/tag.ts
|
|
76
2
|
/**
|
|
77
|
-
*
|
|
78
|
-
* This implements the `.provide()` method logic - only exposes target layer's provisions.
|
|
79
|
-
*
|
|
3
|
+
* Symbol used to identify ValueTag objects at runtime.
|
|
80
4
|
* @internal
|
|
81
5
|
*/
|
|
82
|
-
|
|
83
|
-
return createComposedLayer(dependency$1, target);
|
|
84
|
-
}
|
|
6
|
+
const ValueTagIdKey = "sandly/ValueTagIdKey";
|
|
85
7
|
/**
|
|
86
|
-
*
|
|
87
|
-
* This implements the `.provideMerge()` method logic - exposes both layers' provisions.
|
|
88
|
-
*
|
|
8
|
+
* Symbol used to carry the phantom type for ValueTag.
|
|
89
9
|
* @internal
|
|
90
10
|
*/
|
|
91
|
-
|
|
92
|
-
return layer((container) => {
|
|
93
|
-
const containerWithDependency = dependency$1.register(
|
|
94
|
-
container
|
|
95
|
-
// The type
|
|
96
|
-
// IContainer<TRequires1 | TProvides1 | Exclude<TRequires2, TProvides1> | TContainer>
|
|
97
|
-
// can be simplified to
|
|
98
|
-
// IContainer<TRequires1 | TRequires2 | TProvides1 | TContainer>
|
|
99
|
-
);
|
|
100
|
-
return target.register(containerWithDependency);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
11
|
+
const TagTypeKey = "sandly/TagTypeKey";
|
|
103
12
|
/**
|
|
104
|
-
*
|
|
105
|
-
* This implements the `.merge()` method logic.
|
|
106
|
-
*
|
|
13
|
+
* Helper to get an object property safely.
|
|
107
14
|
* @internal
|
|
108
15
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const container2 = layer2.register(container1);
|
|
113
|
-
return container2;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Utility object containing helper functions for working with layers.
|
|
118
|
-
*/
|
|
119
|
-
const Layer = {
|
|
120
|
-
empty() {
|
|
121
|
-
return layer((container) => container);
|
|
122
|
-
},
|
|
123
|
-
mergeAll(...layers) {
|
|
124
|
-
return layers.reduce((acc, layer$1) => acc.merge(layer$1));
|
|
125
|
-
},
|
|
126
|
-
merge(layer1, layer2) {
|
|
127
|
-
return layer1.merge(layer2);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
//#endregion
|
|
132
|
-
//#region src/constant.ts
|
|
133
|
-
/**
|
|
134
|
-
* Creates a layer that provides a constant value for a given tag.
|
|
135
|
-
*
|
|
136
|
-
* @param tag - The value tag to provide
|
|
137
|
-
* @param constantValue - The constant value to provide
|
|
138
|
-
* @returns A layer with no dependencies that provides the constant value
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```typescript
|
|
142
|
-
* const ApiKey = Tag.of('ApiKey')<string>();
|
|
143
|
-
* const DatabaseUrl = Tag.of('DatabaseUrl')<string>();
|
|
144
|
-
*
|
|
145
|
-
* const apiKey = constant(ApiKey, 'my-secret-key');
|
|
146
|
-
* const dbUrl = constant(DatabaseUrl, 'postgresql://localhost:5432/myapp');
|
|
147
|
-
*
|
|
148
|
-
* const config = Layer.merge(apiKey, dbUrl);
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
function constant(tag, constantValue) {
|
|
152
|
-
return layer((container) => container.register(tag, () => constantValue));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
//#endregion
|
|
156
|
-
//#region src/utils/object.ts
|
|
157
|
-
function hasKey(obj, key) {
|
|
158
|
-
return obj !== void 0 && obj !== null && (typeof obj === "object" || typeof obj === "function") && key in obj;
|
|
159
|
-
}
|
|
160
|
-
function getKey(obj, ...keys) {
|
|
161
|
-
let current = obj;
|
|
162
|
-
for (const key of keys) {
|
|
163
|
-
if (!hasKey(current, key)) return void 0;
|
|
164
|
-
current = current[key];
|
|
165
|
-
}
|
|
166
|
-
return current;
|
|
16
|
+
function getKey(obj, key) {
|
|
17
|
+
if (obj === null || obj === void 0) return void 0;
|
|
18
|
+
return obj[key];
|
|
167
19
|
}
|
|
168
|
-
|
|
169
|
-
//#endregion
|
|
170
|
-
//#region src/tag.ts
|
|
171
|
-
/**
|
|
172
|
-
* Symbol used to identify tagged types within the dependency injection system.
|
|
173
|
-
* This symbol is used as a property key to attach metadata to both value tags and service tags.
|
|
174
|
-
*
|
|
175
|
-
* Note: We can't use a symbol here becuase it produced the following TS error:
|
|
176
|
-
* error TS4020: 'extends' clause of exported class 'NotificationService' has or is using private name 'TagIdKey'.
|
|
177
|
-
*
|
|
178
|
-
* @internal
|
|
179
|
-
*/
|
|
180
|
-
const ValueTagIdKey = "sandly/ValueTagIdKey";
|
|
181
|
-
const ServiceTagIdKey = "sandly/ServiceTagIdKey";
|
|
182
|
-
/**
|
|
183
|
-
* Internal string used to identify the type of a tagged type within the dependency injection system.
|
|
184
|
-
* This string is used as a property key to attach metadata to both value tags and service tags.
|
|
185
|
-
* It is used to carry the type of the tagged type and should not be used directly.
|
|
186
|
-
* @internal
|
|
187
|
-
*/
|
|
188
|
-
const TagTypeKey = "sandly/TagTypeKey";
|
|
189
20
|
/**
|
|
190
|
-
* Utility object
|
|
191
|
-
*
|
|
192
|
-
* The Tag object provides the primary API for creating both value tags and service tags
|
|
193
|
-
* used throughout the dependency injection system. It's the main entry point for
|
|
194
|
-
* defining dependencies in a type-safe way.
|
|
21
|
+
* Utility object for creating and working with tags.
|
|
195
22
|
*/
|
|
196
23
|
const Tag = {
|
|
197
24
|
of: (id) => {
|
|
@@ -200,35 +27,35 @@ const Tag = {
|
|
|
200
27
|
[TagTypeKey]: void 0
|
|
201
28
|
});
|
|
202
29
|
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
30
|
+
id: (tag) => {
|
|
31
|
+
if (typeof tag === "function") {
|
|
32
|
+
const customTag = getKey(tag, "Tag");
|
|
33
|
+
if (customTag !== void 0) return customTag;
|
|
34
|
+
return tag.name || "AnonymousClass";
|
|
207
35
|
}
|
|
208
|
-
return
|
|
36
|
+
return String(tag[ValueTagIdKey]);
|
|
209
37
|
},
|
|
210
|
-
|
|
211
|
-
|
|
38
|
+
isServiceTag: (x) => {
|
|
39
|
+
if (typeof x !== "function") return false;
|
|
40
|
+
return x.prototype !== void 0;
|
|
41
|
+
},
|
|
42
|
+
isValueTag: (x) => {
|
|
43
|
+
return typeof x === "object" && x !== null && getKey(x, ValueTagIdKey) !== void 0;
|
|
212
44
|
},
|
|
213
|
-
isTag: (
|
|
214
|
-
return
|
|
45
|
+
isTag: (x) => {
|
|
46
|
+
return Tag.isServiceTag(x) || Tag.isValueTag(x);
|
|
215
47
|
}
|
|
216
48
|
};
|
|
217
|
-
/**
|
|
218
|
-
* String used to store the original ValueTag in Inject<T> types.
|
|
219
|
-
* This prevents property name collisions while allowing type-level extraction.
|
|
220
|
-
*/
|
|
221
|
-
const InjectSource = "sandly/InjectSource";
|
|
222
49
|
|
|
223
50
|
//#endregion
|
|
224
51
|
//#region src/errors.ts
|
|
225
52
|
/**
|
|
226
|
-
* Base error class for all library errors.
|
|
53
|
+
* Base error class for all Sandly library errors.
|
|
227
54
|
*
|
|
228
|
-
*
|
|
55
|
+
* Extends the native Error class to provide consistent error handling
|
|
229
56
|
* and structured error information across the library.
|
|
230
57
|
*
|
|
231
|
-
* @example
|
|
58
|
+
* @example
|
|
232
59
|
* ```typescript
|
|
233
60
|
* try {
|
|
234
61
|
* await container.resolve(SomeService);
|
|
@@ -248,9 +75,15 @@ var SandlyError = class SandlyError extends Error {
|
|
|
248
75
|
this.detail = detail;
|
|
249
76
|
if (cause instanceof Error && cause.stack !== void 0) this.stack = `${this.stack ?? ""}\nCaused by: ${cause.stack}`;
|
|
250
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Wraps any error as a SandlyError.
|
|
80
|
+
*/
|
|
251
81
|
static ensure(error) {
|
|
252
82
|
return error instanceof SandlyError ? error : new SandlyError("An unknown error occurred", { cause: error });
|
|
253
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns a structured representation of the error for logging.
|
|
86
|
+
*/
|
|
254
87
|
dump() {
|
|
255
88
|
return {
|
|
256
89
|
name: this.name,
|
|
@@ -260,12 +93,14 @@ var SandlyError = class SandlyError extends Error {
|
|
|
260
93
|
cause: this.dumpCause(this.cause)
|
|
261
94
|
};
|
|
262
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Returns a JSON string representation of the error.
|
|
98
|
+
*/
|
|
263
99
|
dumps() {
|
|
264
100
|
return JSON.stringify(this.dump());
|
|
265
101
|
}
|
|
266
102
|
/**
|
|
267
103
|
* Recursively extract cause chain from any Error.
|
|
268
|
-
* Handles both AppError (with dump()) and plain Errors (with cause property).
|
|
269
104
|
*/
|
|
270
105
|
dumpCause(cause) {
|
|
271
106
|
if (cause instanceof SandlyError) return cause.dump();
|
|
@@ -281,34 +116,18 @@ var SandlyError = class SandlyError extends Error {
|
|
|
281
116
|
}
|
|
282
117
|
};
|
|
283
118
|
/**
|
|
284
|
-
* Error thrown when attempting to register a dependency that has already been instantiated.
|
|
285
|
-
*
|
|
286
|
-
* This error occurs when calling `container.register()` for a tag that has already been instantiated.
|
|
287
|
-
* Registration must happen before any instantiation occurs, as cached instances would still be used
|
|
288
|
-
* by existing dependencies.
|
|
289
|
-
*/
|
|
290
|
-
var DependencyAlreadyInstantiatedError = class extends SandlyError {};
|
|
291
|
-
/**
|
|
292
119
|
* Error thrown when attempting to use a container that has been destroyed.
|
|
293
|
-
*
|
|
294
|
-
* This error occurs when calling `container.resolve()`, `container.register()`, or `container.destroy()`
|
|
295
|
-
* on a container that has already been destroyed. It indicates a programming error where the container
|
|
296
|
-
* is being used after it has been destroyed.
|
|
297
120
|
*/
|
|
298
121
|
var ContainerDestroyedError = class extends SandlyError {};
|
|
299
122
|
/**
|
|
300
123
|
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
301
124
|
*
|
|
302
|
-
* This error occurs when calling `container.resolve(Tag)` for a tag that was never
|
|
303
|
-
* registered via `container.register()`. It indicates a programming error where
|
|
304
|
-
* the dependency setup is incomplete.
|
|
305
|
-
*
|
|
306
125
|
* @example
|
|
307
126
|
* ```typescript
|
|
308
|
-
* const container = Container.
|
|
127
|
+
* const container = Container.builder().build(); // Empty container
|
|
309
128
|
*
|
|
310
129
|
* try {
|
|
311
|
-
* await
|
|
130
|
+
* await container.resolve(UnregisteredService);
|
|
312
131
|
* } catch (error) {
|
|
313
132
|
* if (error instanceof UnknownDependencyError) {
|
|
314
133
|
* console.error('Missing dependency:', error.message);
|
|
@@ -317,113 +136,57 @@ var ContainerDestroyedError = class extends SandlyError {};
|
|
|
317
136
|
* ```
|
|
318
137
|
*/
|
|
319
138
|
var UnknownDependencyError = class extends SandlyError {
|
|
320
|
-
/**
|
|
321
|
-
* @internal
|
|
322
|
-
* Creates an UnknownDependencyError for the given tag.
|
|
323
|
-
*
|
|
324
|
-
* @param tag - The dependency tag that wasn't found
|
|
325
|
-
*/
|
|
326
139
|
constructor(tag) {
|
|
327
|
-
super(`No factory registered for dependency ${
|
|
140
|
+
super(`No factory registered for dependency "${Tag.id(tag)}"`);
|
|
328
141
|
}
|
|
329
142
|
};
|
|
330
143
|
/**
|
|
331
|
-
* Error thrown when a circular dependency is detected during
|
|
332
|
-
*
|
|
333
|
-
* This occurs when service A depends on service B, which depends on service A (directly
|
|
334
|
-
* or through a chain of dependencies). The error includes the full dependency chain
|
|
335
|
-
* to help identify the circular reference.
|
|
144
|
+
* Error thrown when a circular dependency is detected during resolution.
|
|
336
145
|
*
|
|
337
|
-
* @example
|
|
146
|
+
* @example
|
|
338
147
|
* ```typescript
|
|
339
|
-
*
|
|
340
|
-
* class ServiceB extends Tag.Service('ServiceB') {}
|
|
341
|
-
*
|
|
342
|
-
* const container = Container.empty()
|
|
343
|
-
* .register(ServiceA, async (ctx) =>
|
|
344
|
-
* new ServiceA(await ctx.resolve(ServiceB)) // Depends on B
|
|
345
|
-
* )
|
|
346
|
-
* .register(ServiceB, async (ctx) =>
|
|
347
|
-
* new ServiceB(await ctx.resolve(ServiceA)) // Depends on A - CIRCULAR!
|
|
348
|
-
* );
|
|
349
|
-
*
|
|
148
|
+
* // ServiceA depends on ServiceB, ServiceB depends on ServiceA
|
|
350
149
|
* try {
|
|
351
|
-
* await
|
|
150
|
+
* await container.resolve(ServiceA);
|
|
352
151
|
* } catch (error) {
|
|
353
152
|
* if (error instanceof CircularDependencyError) {
|
|
354
153
|
* console.error('Circular dependency:', error.message);
|
|
355
|
-
* //
|
|
154
|
+
* // "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
356
155
|
* }
|
|
357
156
|
* }
|
|
358
157
|
* ```
|
|
359
158
|
*/
|
|
360
159
|
var CircularDependencyError = class extends SandlyError {
|
|
361
|
-
/**
|
|
362
|
-
* @internal
|
|
363
|
-
* Creates a CircularDependencyError with the dependency chain information.
|
|
364
|
-
*
|
|
365
|
-
* @param tag - The tag where the circular dependency was detected
|
|
366
|
-
* @param dependencyChain - The chain of dependencies that led to the circular reference
|
|
367
|
-
*/
|
|
368
160
|
constructor(tag, dependencyChain) {
|
|
369
161
|
const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
|
|
370
|
-
super(`Circular dependency detected for ${
|
|
162
|
+
super(`Circular dependency detected for "${Tag.id(tag)}": ${chain} -> ${Tag.id(tag)}`, { detail: {
|
|
371
163
|
tag: Tag.id(tag),
|
|
372
164
|
dependencyChain: dependencyChain.map((t) => Tag.id(t))
|
|
373
165
|
} });
|
|
374
166
|
}
|
|
375
167
|
};
|
|
376
168
|
/**
|
|
377
|
-
* Error thrown when a dependency factory
|
|
169
|
+
* Error thrown when a dependency factory throws during instantiation.
|
|
378
170
|
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
171
|
+
* For nested dependencies (A depends on B depends on C), use `getRootCause()`
|
|
172
|
+
* to unwrap all layers and get the original error.
|
|
381
173
|
*
|
|
382
|
-
*
|
|
383
|
-
* you get nested DependencyCreationErrors. Use `getRootCause()` to get the original error.
|
|
384
|
-
*
|
|
385
|
-
* @example Factory throwing error
|
|
174
|
+
* @example
|
|
386
175
|
* ```typescript
|
|
387
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {}
|
|
388
|
-
*
|
|
389
|
-
* const container = Container.empty().register(DatabaseService, () => {
|
|
390
|
-
* throw new Error('Database connection failed');
|
|
391
|
-
* });
|
|
392
|
-
*
|
|
393
176
|
* try {
|
|
394
|
-
* await
|
|
177
|
+
* await container.resolve(UserService);
|
|
395
178
|
* } catch (error) {
|
|
396
179
|
* if (error instanceof DependencyCreationError) {
|
|
397
180
|
* console.error('Failed to create:', error.message);
|
|
398
|
-
* console.error('Original error:', error.cause);
|
|
399
|
-
* }
|
|
400
|
-
* }
|
|
401
|
-
* ```
|
|
402
|
-
*
|
|
403
|
-
* @example Getting root cause from nested errors
|
|
404
|
-
* ```typescript
|
|
405
|
-
* // ServiceA -> ServiceB -> ServiceC (ServiceC throws)
|
|
406
|
-
* try {
|
|
407
|
-
* await container.resolve(ServiceA);
|
|
408
|
-
* } catch (error) {
|
|
409
|
-
* if (error instanceof DependencyCreationError) {
|
|
410
|
-
* console.error('Top-level error:', error.message); // "Error creating instance of ServiceA"
|
|
411
181
|
* const rootCause = error.getRootCause();
|
|
412
|
-
* console.error('Root cause:', rootCause);
|
|
182
|
+
* console.error('Root cause:', rootCause);
|
|
413
183
|
* }
|
|
414
184
|
* }
|
|
415
185
|
* ```
|
|
416
186
|
*/
|
|
417
187
|
var DependencyCreationError = class DependencyCreationError extends SandlyError {
|
|
418
|
-
/**
|
|
419
|
-
* @internal
|
|
420
|
-
* Creates a DependencyCreationError wrapping the original factory error.
|
|
421
|
-
*
|
|
422
|
-
* @param tag - The tag of the dependency that failed to be created
|
|
423
|
-
* @param error - The original error thrown by the factory function
|
|
424
|
-
*/
|
|
425
188
|
constructor(tag, error) {
|
|
426
|
-
super(`Error creating instance of ${
|
|
189
|
+
super(`Error creating instance of "${Tag.id(tag)}"`, {
|
|
427
190
|
cause: error,
|
|
428
191
|
detail: { tag: Tag.id(tag) }
|
|
429
192
|
});
|
|
@@ -431,22 +194,8 @@ var DependencyCreationError = class DependencyCreationError extends SandlyError
|
|
|
431
194
|
/**
|
|
432
195
|
* Traverses the error chain to find the root cause error.
|
|
433
196
|
*
|
|
434
|
-
* When dependencies are nested, each level wraps the error
|
|
435
|
-
* This method unwraps all
|
|
436
|
-
*
|
|
437
|
-
* @returns The root cause error (not a DependencyCreationError unless that's the only error)
|
|
438
|
-
*
|
|
439
|
-
* @example
|
|
440
|
-
* ```typescript
|
|
441
|
-
* try {
|
|
442
|
-
* await container.resolve(UserService);
|
|
443
|
-
* } catch (error) {
|
|
444
|
-
* if (error instanceof DependencyCreationError) {
|
|
445
|
-
* const rootCause = error.getRootCause();
|
|
446
|
-
* console.error('Root cause:', rootCause);
|
|
447
|
-
* }
|
|
448
|
-
* }
|
|
449
|
-
* ```
|
|
197
|
+
* When dependencies are nested, each level wraps the error.
|
|
198
|
+
* This method unwraps all layers to get the original error.
|
|
450
199
|
*/
|
|
451
200
|
getRootCause() {
|
|
452
201
|
let current = this.cause;
|
|
@@ -457,42 +206,31 @@ var DependencyCreationError = class DependencyCreationError extends SandlyError
|
|
|
457
206
|
/**
|
|
458
207
|
* Error thrown when one or more finalizers fail during container destruction.
|
|
459
208
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* process continues and this error contains details of all failures.
|
|
209
|
+
* Even if some finalizers fail, cleanup continues for all others.
|
|
210
|
+
* This error aggregates all failures.
|
|
463
211
|
*
|
|
464
|
-
* @example
|
|
212
|
+
* @example
|
|
465
213
|
* ```typescript
|
|
466
214
|
* try {
|
|
467
215
|
* await container.destroy();
|
|
468
216
|
* } catch (error) {
|
|
469
217
|
* if (error instanceof DependencyFinalizationError) {
|
|
470
|
-
* console.error('
|
|
471
|
-
* console.error('Error details:', error.detail.errors);
|
|
218
|
+
* console.error('Cleanup failures:', error.getRootCauses());
|
|
472
219
|
* }
|
|
473
220
|
* }
|
|
474
221
|
* ```
|
|
475
222
|
*/
|
|
476
223
|
var DependencyFinalizationError = class extends SandlyError {
|
|
477
|
-
/**
|
|
478
|
-
* @internal
|
|
479
|
-
* Creates a DependencyFinalizationError aggregating multiple finalizer failures.
|
|
480
|
-
*
|
|
481
|
-
* @param errors - Array of errors thrown by individual finalizers
|
|
482
|
-
*/
|
|
483
224
|
constructor(errors) {
|
|
484
|
-
const
|
|
485
|
-
super("Error destroying
|
|
225
|
+
const sandlyErrors = errors.map((error) => SandlyError.ensure(error));
|
|
226
|
+
super("Error destroying container", {
|
|
486
227
|
cause: errors[0],
|
|
487
|
-
detail: { errors:
|
|
228
|
+
detail: { errors: sandlyErrors.map((error) => error.dump()) }
|
|
488
229
|
});
|
|
489
230
|
this.errors = errors;
|
|
490
231
|
}
|
|
491
232
|
/**
|
|
492
|
-
* Returns
|
|
493
|
-
*
|
|
494
|
-
* @returns An array of the errors that occurred during finalization.
|
|
495
|
-
* You can expect at least one error in the array.
|
|
233
|
+
* Returns all root cause errors from the finalization failures.
|
|
496
234
|
*/
|
|
497
235
|
getRootCauses() {
|
|
498
236
|
return this.errors;
|
|
@@ -519,283 +257,199 @@ var ResolutionContextImpl = class {
|
|
|
519
257
|
return results;
|
|
520
258
|
}
|
|
521
259
|
};
|
|
260
|
+
/**
|
|
261
|
+
* Unique symbol for container type branding.
|
|
262
|
+
*/
|
|
522
263
|
const ContainerTypeId = Symbol.for("sandly/Container");
|
|
523
264
|
/**
|
|
524
|
-
*
|
|
525
|
-
* caching, and lifecycle management with support for async dependencies and
|
|
526
|
-
* circular dependency detection.
|
|
265
|
+
* Builder for constructing immutable containers.
|
|
527
266
|
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* and preventing runtime errors.
|
|
267
|
+
* Use `Container.builder()` to create a builder, then chain `.add()` calls
|
|
268
|
+
* to register dependencies, and finally call `.build()` to create the container.
|
|
531
269
|
*
|
|
532
|
-
* @template TTags - Union type of
|
|
270
|
+
* @template TTags - Union type of registered dependency tags
|
|
533
271
|
*
|
|
534
|
-
* @example
|
|
272
|
+
* @example
|
|
535
273
|
* ```typescript
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
* class UserService extends Tag.Service('UserService') {
|
|
543
|
-
* constructor(private db: DatabaseService) {}
|
|
544
|
-
* getUser() { return this.db.query(); }
|
|
545
|
-
* }
|
|
546
|
-
*
|
|
547
|
-
* const container = Container.empty()
|
|
548
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
549
|
-
* .register(UserService, async (ctx) =>
|
|
550
|
-
* new UserService(await ctx.resolve(DatabaseService))
|
|
551
|
-
* );
|
|
552
|
-
*
|
|
553
|
-
* const userService = await c.resolve(UserService);
|
|
274
|
+
* const container = Container.builder()
|
|
275
|
+
* .add(Database, () => new Database())
|
|
276
|
+
* .add(UserService, async (ctx) =>
|
|
277
|
+
* new UserService(await ctx.resolve(Database))
|
|
278
|
+
* )
|
|
279
|
+
* .build();
|
|
554
280
|
* ```
|
|
281
|
+
*/
|
|
282
|
+
var ContainerBuilder = class {
|
|
283
|
+
factories = new Map();
|
|
284
|
+
finalizers = new Map();
|
|
285
|
+
/**
|
|
286
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
287
|
+
*
|
|
288
|
+
* @param tag - The dependency tag (class or ValueTag)
|
|
289
|
+
* @param spec - Factory function or lifecycle object
|
|
290
|
+
* @returns The builder with updated type information
|
|
291
|
+
*/
|
|
292
|
+
add(tag, spec) {
|
|
293
|
+
if (typeof spec === "function") {
|
|
294
|
+
this.factories.set(tag, spec);
|
|
295
|
+
this.finalizers.delete(tag);
|
|
296
|
+
} else {
|
|
297
|
+
this.factories.set(tag, spec.create.bind(spec));
|
|
298
|
+
if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
|
|
299
|
+
else this.finalizers.delete(tag);
|
|
300
|
+
}
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Creates an immutable container from the registered dependencies.
|
|
305
|
+
*/
|
|
306
|
+
build() {
|
|
307
|
+
return Container._createFromBuilder(this.factories, this.finalizers);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Type-safe dependency injection container.
|
|
555
312
|
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
559
|
-
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
560
|
-
*
|
|
561
|
-
* const container = Container.empty()
|
|
562
|
-
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
563
|
-
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
313
|
+
* Containers are immutable - use `Container.builder()` to create one.
|
|
314
|
+
* Each dependency is created once (singleton) and cached.
|
|
564
315
|
*
|
|
565
|
-
*
|
|
566
|
-
* const config = await c.resolve(ConfigTag);
|
|
567
|
-
* ```
|
|
316
|
+
* @template TTags - Union type of registered dependency tags
|
|
568
317
|
*
|
|
569
|
-
* @example
|
|
318
|
+
* @example
|
|
570
319
|
* ```typescript
|
|
571
|
-
* class
|
|
572
|
-
*
|
|
573
|
-
* async disconnect() { return; }
|
|
320
|
+
* class Database {
|
|
321
|
+
* query(sql: string) { return []; }
|
|
574
322
|
* }
|
|
575
323
|
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* async (
|
|
584
|
-
* )
|
|
324
|
+
* class UserService {
|
|
325
|
+
* constructor(private db: Database) {}
|
|
326
|
+
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
327
|
+
* }
|
|
328
|
+
*
|
|
329
|
+
* const container = Container.builder()
|
|
330
|
+
* .add(Database, () => new Database())
|
|
331
|
+
* .add(UserService, async (ctx) =>
|
|
332
|
+
* new UserService(await ctx.resolve(Database))
|
|
333
|
+
* )
|
|
334
|
+
* .build();
|
|
585
335
|
*
|
|
586
|
-
*
|
|
587
|
-
* await c.destroy(); // Calls all finalizers
|
|
336
|
+
* const userService = await container.resolve(UserService);
|
|
588
337
|
* ```
|
|
589
338
|
*/
|
|
590
339
|
var Container = class Container {
|
|
591
340
|
[ContainerTypeId];
|
|
592
|
-
constructor() {}
|
|
593
341
|
/**
|
|
594
|
-
* Cache of instantiated dependencies
|
|
595
|
-
* Ensures singleton behavior and supports concurrent access.
|
|
342
|
+
* Cache of instantiated dependencies.
|
|
596
343
|
* @internal
|
|
597
344
|
*/
|
|
598
345
|
cache = new Map();
|
|
599
346
|
/**
|
|
600
|
-
* Factory functions for creating
|
|
347
|
+
* Factory functions for creating dependencies.
|
|
601
348
|
* @internal
|
|
602
349
|
*/
|
|
603
|
-
factories
|
|
350
|
+
factories;
|
|
604
351
|
/**
|
|
605
|
-
*
|
|
352
|
+
* Cleanup functions for dependencies.
|
|
606
353
|
* @internal
|
|
607
354
|
*/
|
|
608
|
-
finalizers
|
|
355
|
+
finalizers;
|
|
609
356
|
/**
|
|
610
|
-
*
|
|
357
|
+
* Whether this container has been destroyed.
|
|
611
358
|
* @internal
|
|
612
359
|
*/
|
|
613
360
|
isDestroyed = false;
|
|
614
361
|
/**
|
|
615
|
-
*
|
|
616
|
-
* @returns A new empty Container instance with no registered dependencies.
|
|
362
|
+
* @internal - Use Container.builder() or Container.empty()
|
|
617
363
|
*/
|
|
618
|
-
|
|
619
|
-
|
|
364
|
+
constructor(factories, finalizers) {
|
|
365
|
+
this.factories = factories;
|
|
366
|
+
this.finalizers = finalizers;
|
|
620
367
|
}
|
|
621
368
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
* The factory function receives the current container instance and must return the
|
|
625
|
-
* service instance (or a Promise of it). The container tracks the registration at
|
|
626
|
-
* the type level, ensuring type safety for subsequent `.resolve()` calls.
|
|
627
|
-
*
|
|
628
|
-
* If a dependency is already registered, this method will override it unless the
|
|
629
|
-
* dependency has already been instantiated, in which case it will throw an error.
|
|
630
|
-
*
|
|
631
|
-
* @template T - The dependency tag being registered
|
|
632
|
-
* @param tag - The dependency tag (class or value tag)
|
|
633
|
-
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
634
|
-
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
635
|
-
* @returns A new container instance with the dependency registered
|
|
636
|
-
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
637
|
-
* @throws {Error} If the dependency has already been instantiated
|
|
638
|
-
*
|
|
639
|
-
* @example Registering a simple service
|
|
640
|
-
* ```typescript
|
|
641
|
-
* class LoggerService extends Tag.Service('LoggerService') {
|
|
642
|
-
* log(message: string) { console.log(message); }
|
|
643
|
-
* }
|
|
644
|
-
*
|
|
645
|
-
* const container = Container.empty().register(
|
|
646
|
-
* LoggerService,
|
|
647
|
-
* () => new LoggerService()
|
|
648
|
-
* );
|
|
649
|
-
* ```
|
|
650
|
-
*
|
|
651
|
-
* @example Registering with dependencies
|
|
652
|
-
* ```typescript
|
|
653
|
-
* class UserService extends Tag.Service('UserService') {
|
|
654
|
-
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
655
|
-
* }
|
|
656
|
-
*
|
|
657
|
-
* const container = Container.empty()
|
|
658
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
659
|
-
* .register(LoggerService, () => new LoggerService())
|
|
660
|
-
* .register(UserService, async (ctx) =>
|
|
661
|
-
* new UserService(
|
|
662
|
-
* await ctx.resolve(DatabaseService),
|
|
663
|
-
* await ctx.resolve(LoggerService)
|
|
664
|
-
* )
|
|
665
|
-
* );
|
|
666
|
-
* ```
|
|
667
|
-
*
|
|
668
|
-
* @example Overriding a dependency
|
|
669
|
-
* ```typescript
|
|
670
|
-
* const container = Container.empty()
|
|
671
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
672
|
-
* .register(DatabaseService, () => new MockDatabaseService()); // Overrides the previous registration
|
|
673
|
-
* ```
|
|
674
|
-
*
|
|
675
|
-
* @example Using value tags
|
|
676
|
-
* ```typescript
|
|
677
|
-
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
678
|
-
*
|
|
679
|
-
* const container = Container.empty().register(
|
|
680
|
-
* ConfigTag,
|
|
681
|
-
* () => ({ apiUrl: 'https://api.example.com' })
|
|
682
|
-
* );
|
|
683
|
-
* ```
|
|
684
|
-
*
|
|
685
|
-
* @example With finalizer for cleanup
|
|
686
|
-
* ```typescript
|
|
687
|
-
* class DatabaseConnection extends Tag.Service('DatabaseConnection') {
|
|
688
|
-
* async connect() { return; }
|
|
689
|
-
* async close() { return; }
|
|
690
|
-
* }
|
|
691
|
-
*
|
|
692
|
-
* const container = Container.empty().register(
|
|
693
|
-
* DatabaseConnection,
|
|
694
|
-
* async () => {
|
|
695
|
-
* const conn = new DatabaseConnection();
|
|
696
|
-
* await conn.connect();
|
|
697
|
-
* return conn;
|
|
698
|
-
* },
|
|
699
|
-
* (conn) => conn.close() // Called during container.destroy()
|
|
700
|
-
* );
|
|
701
|
-
* ```
|
|
369
|
+
* @internal - Used by ContainerBuilder
|
|
702
370
|
*/
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
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.`);
|
|
706
|
-
if (typeof spec === "function") {
|
|
707
|
-
this.factories.set(tag, spec);
|
|
708
|
-
this.finalizers.delete(tag);
|
|
709
|
-
} else {
|
|
710
|
-
this.factories.set(tag, spec.create.bind(spec));
|
|
711
|
-
if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
|
|
712
|
-
else this.finalizers.delete(tag);
|
|
713
|
-
}
|
|
714
|
-
return this;
|
|
371
|
+
static _createFromBuilder(factories, finalizers) {
|
|
372
|
+
return new Container(factories, finalizers);
|
|
715
373
|
}
|
|
716
374
|
/**
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* This returns `true` if the dependency has been registered via `.register()`,
|
|
720
|
-
* regardless of whether it has been instantiated yet.
|
|
721
|
-
*
|
|
722
|
-
* @param tag - The dependency tag to check
|
|
723
|
-
* @returns `true` if the dependency has been registered, `false` otherwise
|
|
375
|
+
* Creates a new container builder.
|
|
724
376
|
*
|
|
725
377
|
* @example
|
|
726
378
|
* ```typescript
|
|
727
|
-
* const container = Container.
|
|
728
|
-
*
|
|
379
|
+
* const container = Container.builder()
|
|
380
|
+
* .add(Database, () => new Database())
|
|
381
|
+
* .build();
|
|
729
382
|
* ```
|
|
730
383
|
*/
|
|
731
|
-
|
|
732
|
-
return
|
|
384
|
+
static builder() {
|
|
385
|
+
return new ContainerBuilder();
|
|
733
386
|
}
|
|
734
387
|
/**
|
|
735
|
-
*
|
|
388
|
+
* Creates an empty container with no dependencies.
|
|
736
389
|
*
|
|
737
|
-
*
|
|
738
|
-
* @returns true if the dependency has been instantiated, false otherwise
|
|
390
|
+
* Shorthand for `Container.builder().build()`.
|
|
739
391
|
*/
|
|
740
|
-
|
|
741
|
-
return
|
|
392
|
+
static empty() {
|
|
393
|
+
return Container.builder().build();
|
|
742
394
|
}
|
|
743
395
|
/**
|
|
744
|
-
*
|
|
396
|
+
* Creates a scoped container for hierarchical dependency management.
|
|
745
397
|
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* requests for the same dependency correctly.
|
|
398
|
+
* Scoped containers support parent/child relationships where children
|
|
399
|
+
* can access parent dependencies but maintain their own cache.
|
|
749
400
|
*
|
|
750
|
-
*
|
|
751
|
-
* through the resolution context.
|
|
401
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
752
402
|
*
|
|
753
|
-
* @
|
|
754
|
-
* @param tag - The dependency tag to retrieve
|
|
755
|
-
* @returns Promise resolving to the service instance
|
|
756
|
-
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
757
|
-
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
758
|
-
* @throws {DependencyCreationError} If the factory function throws an error
|
|
759
|
-
*
|
|
760
|
-
* @example Basic usage
|
|
403
|
+
* @example
|
|
761
404
|
* ```typescript
|
|
762
|
-
* const
|
|
763
|
-
*
|
|
405
|
+
* const appContainer = Container.scoped('app');
|
|
406
|
+
* // ... add app-level dependencies
|
|
764
407
|
*
|
|
765
|
-
* const
|
|
766
|
-
*
|
|
408
|
+
* const requestContainer = appContainer.child('request');
|
|
409
|
+
* // ... add request-specific dependencies
|
|
767
410
|
* ```
|
|
411
|
+
*/
|
|
412
|
+
static scoped(scope) {
|
|
413
|
+
return ScopedContainer.empty(scope);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Creates a container from a layer.
|
|
768
417
|
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
* // All three calls will receive the same instance
|
|
772
|
-
* const [db1, db2, db3] = await Promise.all([
|
|
773
|
-
* c.resolve(DatabaseService),
|
|
774
|
-
* c.resolve(DatabaseService),
|
|
775
|
-
* c.resolve(DatabaseService)
|
|
776
|
-
* ]);
|
|
418
|
+
* This is a convenience method equivalent to applying a layer to
|
|
419
|
+
* `Container.builder()` and building the result.
|
|
777
420
|
*
|
|
778
|
-
*
|
|
779
|
-
* ```
|
|
421
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
780
422
|
*
|
|
781
|
-
* @example
|
|
423
|
+
* @example
|
|
782
424
|
* ```typescript
|
|
783
|
-
* const
|
|
784
|
-
*
|
|
785
|
-
* .register(UserService, async (ctx) => {
|
|
786
|
-
* const db = await ctx.resolve(DatabaseService);
|
|
787
|
-
* return new UserService(db);
|
|
788
|
-
* });
|
|
425
|
+
* const dbLayer = Layer.service(Database, []);
|
|
426
|
+
* const container = Container.from(dbLayer);
|
|
789
427
|
*
|
|
790
|
-
* const
|
|
428
|
+
* const db = await container.resolve(Database);
|
|
791
429
|
* ```
|
|
792
430
|
*/
|
|
431
|
+
static from(layer) {
|
|
432
|
+
const builder = Container.builder();
|
|
433
|
+
const resultBuilder = layer.apply(builder);
|
|
434
|
+
return resultBuilder.build();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Resolves a dependency, creating it if necessary.
|
|
438
|
+
*
|
|
439
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
440
|
+
*
|
|
441
|
+
* @param tag - The dependency tag to resolve
|
|
442
|
+
* @returns Promise resolving to the dependency instance
|
|
443
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
444
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
445
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
446
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
447
|
+
*/
|
|
793
448
|
async resolve(tag) {
|
|
794
449
|
return this.resolveInternal(tag, []);
|
|
795
450
|
}
|
|
796
451
|
/**
|
|
797
|
-
* Internal resolution
|
|
798
|
-
* Can be overridden by subclasses (e.g., ScopedContainer) to implement custom resolution logic.
|
|
452
|
+
* Internal resolution with dependency chain tracking.
|
|
799
453
|
* @internal
|
|
800
454
|
*/
|
|
801
455
|
resolveInternal(tag, chain) {
|
|
@@ -806,7 +460,7 @@ var Container = class Container {
|
|
|
806
460
|
const factory = this.factories.get(tag);
|
|
807
461
|
if (factory === void 0) throw new UnknownDependencyError(tag);
|
|
808
462
|
const newChain = [...chain, tag];
|
|
809
|
-
const context = new ResolutionContextImpl((
|
|
463
|
+
const context = new ResolutionContextImpl((t) => this.resolveInternal(t, newChain));
|
|
810
464
|
const instancePromise = (async () => {
|
|
811
465
|
try {
|
|
812
466
|
const instance = await factory(context);
|
|
@@ -822,44 +476,14 @@ var Container = class Container {
|
|
|
822
476
|
return instancePromise;
|
|
823
477
|
}
|
|
824
478
|
/**
|
|
825
|
-
* Resolves multiple dependencies concurrently
|
|
479
|
+
* Resolves multiple dependencies concurrently.
|
|
826
480
|
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* The method maintains all the same guarantees as the individual resolve method:
|
|
830
|
-
* singleton behavior, circular dependency detection, and proper error handling.
|
|
831
|
-
*
|
|
832
|
-
* @template T - The tuple type of dependency tags to resolve
|
|
833
|
-
* @param tags - Variable number of dependency tags to resolve
|
|
834
|
-
* @returns Promise resolving to a tuple of service instances in the same order
|
|
481
|
+
* @param tags - The dependency tags to resolve
|
|
482
|
+
* @returns Promise resolving to a tuple of instances
|
|
835
483
|
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
836
484
|
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
837
485
|
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
838
486
|
* @throws {DependencyCreationError} If any factory function throws an error
|
|
839
|
-
*
|
|
840
|
-
* @example Basic usage
|
|
841
|
-
* ```typescript
|
|
842
|
-
* const container = Container.empty()
|
|
843
|
-
* .register(DatabaseService, () => new DatabaseService())
|
|
844
|
-
* .register(LoggerService, () => new LoggerService());
|
|
845
|
-
*
|
|
846
|
-
* const [db, logger] = await c.resolveAll(DatabaseService, LoggerService);
|
|
847
|
-
* ```
|
|
848
|
-
*
|
|
849
|
-
* @example Mixed tag types
|
|
850
|
-
* ```typescript
|
|
851
|
-
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
852
|
-
* const container = Container.empty()
|
|
853
|
-
* .register(ApiKeyTag, () => 'secret-key')
|
|
854
|
-
* .register(UserService, () => new UserService());
|
|
855
|
-
*
|
|
856
|
-
* const [apiKey, userService] = await c.resolveAll(ApiKeyTag, UserService);
|
|
857
|
-
* ```
|
|
858
|
-
*
|
|
859
|
-
* @example Empty array
|
|
860
|
-
* ```typescript
|
|
861
|
-
* const results = await c.resolveAll(); // Returns empty array
|
|
862
|
-
* ```
|
|
863
487
|
*/
|
|
864
488
|
async resolveAll(...tags) {
|
|
865
489
|
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot resolve dependencies from a destroyed container");
|
|
@@ -868,75 +492,45 @@ var Container = class Container {
|
|
|
868
492
|
return results;
|
|
869
493
|
}
|
|
870
494
|
/**
|
|
871
|
-
*
|
|
495
|
+
* Resolves a service, runs the callback with it, then destroys the container.
|
|
872
496
|
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* This ensures proper cleanup and prevents runtime errors from accessing destroyed resources.
|
|
876
|
-
*
|
|
877
|
-
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
878
|
-
* for maximum cleanup performance.
|
|
879
|
-
* If any finalizers fail, all errors are collected and a DependencyFinalizationError
|
|
880
|
-
* is thrown containing details of all failures.
|
|
881
|
-
*
|
|
882
|
-
* **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
|
|
883
|
-
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
884
|
-
* dependencies are cleaned up.
|
|
497
|
+
* This is a convenience method for the common "create, use, destroy" pattern.
|
|
498
|
+
* The container is always destroyed after the callback completes, even if it throws.
|
|
885
499
|
*
|
|
886
|
-
* @
|
|
887
|
-
* @
|
|
500
|
+
* @param tag - The dependency tag to resolve
|
|
501
|
+
* @param fn - Callback that receives the resolved service
|
|
502
|
+
* @returns Promise resolving to the callback's return value
|
|
503
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
504
|
+
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
505
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
506
|
+
* @throws {DependencyCreationError} If the factory function throws
|
|
507
|
+
* @throws {DependencyFinalizationError} If the finalizer function throws
|
|
888
508
|
*
|
|
889
|
-
* @example
|
|
509
|
+
* @example
|
|
890
510
|
* ```typescript
|
|
891
|
-
* const
|
|
892
|
-
* .
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
* await conn.connect();
|
|
896
|
-
* return conn;
|
|
897
|
-
* },
|
|
898
|
-
* (conn) => conn.disconnect() // Finalizer
|
|
899
|
-
* );
|
|
900
|
-
*
|
|
901
|
-
* const db = await c.resolve(DatabaseConnection);
|
|
902
|
-
* await c.destroy(); // Calls conn.disconnect(), container becomes unusable
|
|
903
|
-
*
|
|
904
|
-
* // This will throw an error
|
|
905
|
-
* try {
|
|
906
|
-
* await c.resolve(DatabaseConnection);
|
|
907
|
-
* } catch (error) {
|
|
908
|
-
* console.log(error.message); // "Cannot resolve dependencies from a destroyed container"
|
|
909
|
-
* }
|
|
511
|
+
* const result = await container.use(UserService, (service) =>
|
|
512
|
+
* service.getUsers()
|
|
513
|
+
* );
|
|
514
|
+
* // Container is automatically destroyed after callback completes
|
|
910
515
|
* ```
|
|
516
|
+
*/
|
|
517
|
+
async use(tag, fn) {
|
|
518
|
+
try {
|
|
519
|
+
const service = await this.resolve(tag);
|
|
520
|
+
return await fn(service);
|
|
521
|
+
} finally {
|
|
522
|
+
await this.destroy();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Destroys the container, calling all finalizers.
|
|
911
527
|
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
* .register(HTTPServer, async (ctx) => new HTTPServer(await ctx.resolve(DatabaseService)));
|
|
917
|
-
*
|
|
918
|
-
* // During application shutdown
|
|
919
|
-
* process.on('SIGTERM', async () => {
|
|
920
|
-
* try {
|
|
921
|
-
* await appContainer.destroy(); // Clean shutdown of all services
|
|
922
|
-
* } catch (error) {
|
|
923
|
-
* console.error('Error during shutdown:', error);
|
|
924
|
-
* }
|
|
925
|
-
* process.exit(0);
|
|
926
|
-
* });
|
|
927
|
-
* ```
|
|
528
|
+
* After destruction, the container cannot be used.
|
|
529
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
530
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
531
|
+
* dependencies are cleaned up.
|
|
928
532
|
*
|
|
929
|
-
* @
|
|
930
|
-
* ```typescript
|
|
931
|
-
* try {
|
|
932
|
-
* await container.destroy();
|
|
933
|
-
* } catch (error) {
|
|
934
|
-
* if (error instanceof DependencyContainerFinalizationError) {
|
|
935
|
-
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
936
|
-
* }
|
|
937
|
-
* }
|
|
938
|
-
* // Container is destroyed regardless of finalizer errors
|
|
939
|
-
* ```
|
|
533
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
940
534
|
*/
|
|
941
535
|
async destroy() {
|
|
942
536
|
if (this.isDestroyed) return;
|
|
@@ -946,167 +540,153 @@ var Container = class Container {
|
|
|
946
540
|
return finalizer(dep);
|
|
947
541
|
});
|
|
948
542
|
const results = await Promise.allSettled(promises);
|
|
949
|
-
const failures = results.filter((
|
|
950
|
-
if (failures.length > 0) throw new DependencyFinalizationError(failures.map((
|
|
543
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
544
|
+
if (failures.length > 0) throw new DependencyFinalizationError(failures.map((r) => r.reason));
|
|
951
545
|
} finally {
|
|
952
546
|
this.isDestroyed = true;
|
|
953
547
|
this.cache.clear();
|
|
954
548
|
}
|
|
955
549
|
}
|
|
956
550
|
};
|
|
957
|
-
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/dependency.ts
|
|
960
551
|
/**
|
|
961
|
-
*
|
|
552
|
+
* Builder for constructing scoped containers.
|
|
962
553
|
*
|
|
963
|
-
*
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
*
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
554
|
+
* @template TTags - Union type of registered dependency tags
|
|
555
|
+
*/
|
|
556
|
+
var ScopedContainerBuilder = class {
|
|
557
|
+
factories = new Map();
|
|
558
|
+
finalizers = new Map();
|
|
559
|
+
constructor(scope, parent) {
|
|
560
|
+
this.scope = scope;
|
|
561
|
+
this.parent = parent;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Registers a dependency with a factory function or lifecycle object.
|
|
565
|
+
*/
|
|
566
|
+
add(tag, spec) {
|
|
567
|
+
if (typeof spec === "function") {
|
|
568
|
+
this.factories.set(tag, spec);
|
|
569
|
+
this.finalizers.delete(tag);
|
|
570
|
+
} else {
|
|
571
|
+
this.factories.set(tag, spec.create.bind(spec));
|
|
572
|
+
if (spec.cleanup) this.finalizers.set(tag, spec.cleanup.bind(spec));
|
|
573
|
+
else this.finalizers.delete(tag);
|
|
574
|
+
}
|
|
575
|
+
return this;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Creates an immutable scoped container from the registered dependencies.
|
|
579
|
+
*/
|
|
580
|
+
build() {
|
|
581
|
+
const child = ScopedContainer._createScopedFromBuilder(this.scope, this.parent, this.factories, this.finalizers);
|
|
582
|
+
if (this.parent instanceof ScopedContainer) this.parent._registerChild(child);
|
|
583
|
+
return child;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Scoped container for hierarchical dependency management.
|
|
985
588
|
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
* Database,
|
|
990
|
-
* async (ctx) => {
|
|
991
|
-
* const config = await ctx.resolve(Config);
|
|
992
|
-
* const logger = await ctx.resolve(Logger);
|
|
993
|
-
* logger.info('Creating database connection');
|
|
994
|
-
* return createDb(config.DATABASE);
|
|
995
|
-
* },
|
|
996
|
-
* [Config, Logger]
|
|
997
|
-
* );
|
|
998
|
-
* ```
|
|
589
|
+
* Supports parent/child relationships where children can access parent
|
|
590
|
+
* dependencies but maintain their own cache. Useful for request-scoped
|
|
591
|
+
* dependencies in web applications.
|
|
999
592
|
*
|
|
1000
|
-
* @
|
|
1001
|
-
* ```typescript
|
|
1002
|
-
* const database = dependency(
|
|
1003
|
-
* Database,
|
|
1004
|
-
* {
|
|
1005
|
-
* create: async (ctx) => {
|
|
1006
|
-
* const config = await ctx.resolve(Config);
|
|
1007
|
-
* const logger = await ctx.resolve(Logger);
|
|
1008
|
-
* logger.info('Creating database connection');
|
|
1009
|
-
* return await createDb(config.DATABASE);
|
|
1010
|
-
* },
|
|
1011
|
-
* cleanup: async (db) => {
|
|
1012
|
-
* await disconnectDb(db);
|
|
1013
|
-
* },
|
|
1014
|
-
* },
|
|
1015
|
-
* [Config, Logger]
|
|
1016
|
-
* );
|
|
1017
|
-
* ```
|
|
593
|
+
* @template TTags - Union type of registered dependency tags
|
|
1018
594
|
*
|
|
1019
|
-
* @example
|
|
595
|
+
* @example
|
|
1020
596
|
* ```typescript
|
|
1021
|
-
* //
|
|
1022
|
-
* const
|
|
1023
|
-
* (
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
* )
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* const config = await ctx.resolve(Config);
|
|
1035
|
-
* return createDb(config.DATABASE);
|
|
1036
|
-
* },
|
|
1037
|
-
* [Config, Logger]
|
|
1038
|
-
* );
|
|
597
|
+
* // Application-level container
|
|
598
|
+
* const appContainer = ScopedContainer.builder('app')
|
|
599
|
+
* .add(Database, () => new Database())
|
|
600
|
+
* .build();
|
|
601
|
+
*
|
|
602
|
+
* // Request-level container (inherits from app)
|
|
603
|
+
* const requestContainer = appContainer.child('request')
|
|
604
|
+
* .add(RequestContext, () => new RequestContext())
|
|
605
|
+
* .build();
|
|
606
|
+
*
|
|
607
|
+
* // Can resolve both app and request dependencies
|
|
608
|
+
* const db = await requestContainer.resolve(Database);
|
|
609
|
+
* const ctx = await requestContainer.resolve(RequestContext);
|
|
1039
610
|
* ```
|
|
1040
611
|
*/
|
|
1041
|
-
function dependency(tag, spec, requirements) {
|
|
1042
|
-
return layer((container) => {
|
|
1043
|
-
return container.register(tag, spec);
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
//#endregion
|
|
1048
|
-
//#region src/scoped-container.ts
|
|
1049
612
|
var ScopedContainer = class ScopedContainer extends Container {
|
|
1050
613
|
scope;
|
|
1051
614
|
parent;
|
|
1052
615
|
children = [];
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
616
|
+
/**
|
|
617
|
+
* @internal
|
|
618
|
+
*/
|
|
619
|
+
constructor(scope, parent, factories, finalizers) {
|
|
620
|
+
super(factories, finalizers);
|
|
1056
621
|
this.scope = scope;
|
|
622
|
+
this.parent = parent;
|
|
1057
623
|
}
|
|
1058
624
|
/**
|
|
1059
|
-
*
|
|
1060
|
-
* @param scope - The scope identifier for this container
|
|
1061
|
-
* @returns A new empty ScopedContainer instance with no registered dependencies
|
|
625
|
+
* @internal - Used by ScopedContainerBuilder
|
|
1062
626
|
*/
|
|
1063
|
-
static
|
|
1064
|
-
return new ScopedContainer(
|
|
627
|
+
static _createScopedFromBuilder(scope, parent, factories, finalizers) {
|
|
628
|
+
return new ScopedContainer(scope, parent, factories, finalizers);
|
|
1065
629
|
}
|
|
1066
630
|
/**
|
|
1067
|
-
*
|
|
631
|
+
* Creates a new scoped container builder.
|
|
1068
632
|
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
633
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
634
|
+
*
|
|
635
|
+
* @example
|
|
636
|
+
* ```typescript
|
|
637
|
+
* const container = ScopedContainer.builder('app')
|
|
638
|
+
* .add(Database, () => new Database())
|
|
639
|
+
* .build();
|
|
640
|
+
* ```
|
|
1071
641
|
*/
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
return this;
|
|
642
|
+
static builder(scope) {
|
|
643
|
+
return new ScopedContainerBuilder(scope, null);
|
|
1075
644
|
}
|
|
1076
645
|
/**
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
* This method checks the current scope first, then walks up the parent chain.
|
|
1080
|
-
* Returns true if the dependency has been registered somewhere in the scope hierarchy.
|
|
646
|
+
* Creates an empty scoped container with no dependencies.
|
|
1081
647
|
*/
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
return this.parent?.has(tag) ?? false;
|
|
648
|
+
static empty(scope) {
|
|
649
|
+
return ScopedContainer.builder(scope).build();
|
|
1085
650
|
}
|
|
1086
651
|
/**
|
|
1087
|
-
*
|
|
652
|
+
* Creates a scoped container from a layer.
|
|
1088
653
|
*
|
|
1089
|
-
* This
|
|
1090
|
-
*
|
|
654
|
+
* This is a convenience method equivalent to applying a layer to
|
|
655
|
+
* `ScopedContainer.builder()` and building the result.
|
|
656
|
+
*
|
|
657
|
+
* @param scope - Identifier for the scope (for debugging)
|
|
658
|
+
* @param layer - A layer with no requirements (all dependencies satisfied)
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* const dbLayer = Layer.service(Database, []);
|
|
663
|
+
* const container = ScopedContainer.from('app', dbLayer);
|
|
664
|
+
*
|
|
665
|
+
* const db = await container.resolve(Database);
|
|
666
|
+
* ```
|
|
1091
667
|
*/
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
668
|
+
static from(scope, layer) {
|
|
669
|
+
const builder = ScopedContainer.builder(scope);
|
|
670
|
+
const resultBuilder = layer.apply(builder);
|
|
671
|
+
return resultBuilder.build();
|
|
1095
672
|
}
|
|
1096
673
|
/**
|
|
1097
|
-
*
|
|
674
|
+
* Resolves a dependency from this scope or parent scopes, creating it if necessary.
|
|
1098
675
|
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
676
|
+
* Dependencies are singletons - the same instance is returned on subsequent calls.
|
|
677
|
+
*
|
|
678
|
+
* @param tag - The dependency tag to resolve
|
|
679
|
+
* @returns Promise resolving to the dependency instance
|
|
680
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
681
|
+
* @throws {UnknownDependencyError} If any dependency is not registered
|
|
682
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
683
|
+
* @throws {DependencyCreationError} If any factory function throws an error
|
|
1104
684
|
*/
|
|
1105
685
|
async resolve(tag) {
|
|
1106
686
|
return this.resolveInternal(tag, []);
|
|
1107
687
|
}
|
|
1108
688
|
/**
|
|
1109
|
-
* Internal resolution with delegation
|
|
689
|
+
* Internal resolution with parent delegation.
|
|
1110
690
|
* @internal
|
|
1111
691
|
*/
|
|
1112
692
|
resolveInternal(tag, chain) {
|
|
@@ -1115,22 +695,76 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
1115
695
|
throw new UnknownDependencyError(tag);
|
|
1116
696
|
}
|
|
1117
697
|
/**
|
|
1118
|
-
*
|
|
698
|
+
* @internal - Used by ScopedContainerBuilder to register children
|
|
699
|
+
*/
|
|
700
|
+
_registerChild(child) {
|
|
701
|
+
this.children.push(new WeakRef(child));
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Creates a child container builder that inherits from this container.
|
|
1119
705
|
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
* 2. Then calls finalizers for dependencies created in this scope
|
|
1123
|
-
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
706
|
+
* Use this to create a child scope and add dependencies to it.
|
|
707
|
+
* The child can resolve dependencies from this container.
|
|
1124
708
|
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
709
|
+
* @param scope - Identifier for the child scope
|
|
710
|
+
* @returns A new ScopedContainerBuilder for the child scope
|
|
711
|
+
* @throws {ContainerDestroyedError} If the container has been destroyed
|
|
712
|
+
*
|
|
713
|
+
* @example
|
|
714
|
+
* ```typescript
|
|
715
|
+
* const requestContainer = appContainer.child('request')
|
|
716
|
+
* .add(RequestContext, () => new RequestContext())
|
|
717
|
+
* .build();
|
|
718
|
+
*
|
|
719
|
+
* await requestContainer.resolve(Database); // From parent
|
|
720
|
+
* await requestContainer.resolve(RequestContext); // From this scope
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
child(scope) {
|
|
724
|
+
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
|
|
725
|
+
return new ScopedContainerBuilder(scope, this);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Creates a child container with a layer applied.
|
|
729
|
+
*
|
|
730
|
+
* This is a convenience method combining child() + layer.apply() + build().
|
|
731
|
+
* Use this when you have a layer ready to apply.
|
|
732
|
+
*
|
|
733
|
+
* @param scope - Identifier for the child scope
|
|
734
|
+
* @param layer - Layer to apply to the child (can require parent's tags)
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* const requestContainer = appContainer.childFrom('request',
|
|
739
|
+
* userService
|
|
740
|
+
* .provide(Layer.value(TenantContext, tenantCtx))
|
|
741
|
+
* .provide(Layer.value(RequestId, requestId))
|
|
742
|
+
* );
|
|
743
|
+
*
|
|
744
|
+
* const users = await requestContainer.resolve(UserService);
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
childFrom(scope, layer) {
|
|
748
|
+
return layer.apply(this.child(scope)).build();
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Destroys this container and all child containers.
|
|
752
|
+
*
|
|
753
|
+
* Children are destroyed first to ensure proper cleanup order.
|
|
754
|
+
*
|
|
755
|
+
* After destruction, the container cannot be used.
|
|
756
|
+
* Finalizers run concurrently, so there are no ordering guarantees.
|
|
757
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
758
|
+
* dependencies are cleaned up.
|
|
759
|
+
*
|
|
760
|
+
* @throws {DependencyFinalizationError} If any finalizers fail
|
|
1127
761
|
*/
|
|
1128
762
|
async destroy() {
|
|
1129
763
|
if (this.isDestroyed) return;
|
|
1130
764
|
const allFailures = [];
|
|
1131
|
-
const childDestroyPromises = this.children.map((
|
|
765
|
+
const childDestroyPromises = this.children.map((ref) => ref.deref()).filter((child) => child !== void 0).map((child) => child.destroy());
|
|
1132
766
|
const childResults = await Promise.allSettled(childDestroyPromises);
|
|
1133
|
-
const childFailures = childResults.filter((
|
|
767
|
+
const childFailures = childResults.filter((r) => r.status === "rejected").map((r) => r.reason);
|
|
1134
768
|
allFailures.push(...childFailures);
|
|
1135
769
|
try {
|
|
1136
770
|
await super.destroy();
|
|
@@ -1141,185 +775,152 @@ var ScopedContainer = class ScopedContainer extends Container {
|
|
|
1141
775
|
}
|
|
1142
776
|
if (allFailures.length > 0) throw new DependencyFinalizationError(allFailures);
|
|
1143
777
|
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Creates a child scoped container.
|
|
1146
|
-
*
|
|
1147
|
-
* Child containers inherit access to parent dependencies but maintain
|
|
1148
|
-
* their own scope for new registrations and instance caching.
|
|
1149
|
-
*/
|
|
1150
|
-
child(scope) {
|
|
1151
|
-
if (this.isDestroyed) throw new ContainerDestroyedError("Cannot create child containers from a destroyed container");
|
|
1152
|
-
const child = new ScopedContainer(this, scope);
|
|
1153
|
-
this.children.push(new WeakRef(child));
|
|
1154
|
-
return child;
|
|
1155
|
-
}
|
|
1156
778
|
};
|
|
1157
779
|
|
|
1158
780
|
//#endregion
|
|
1159
|
-
//#region src/
|
|
781
|
+
//#region src/layer.ts
|
|
1160
782
|
/**
|
|
1161
|
-
*
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1187
|
-
* query() { return []; }
|
|
1188
|
-
* }
|
|
1189
|
-
*
|
|
1190
|
-
* class UserService extends Tag.Service('UserService') {
|
|
1191
|
-
* constructor(private db: DatabaseService) {
|
|
1192
|
-
* super();
|
|
1193
|
-
* }
|
|
1194
|
-
*
|
|
1195
|
-
* getUsers() { return this.db.query(); }
|
|
1196
|
-
* }
|
|
1197
|
-
*
|
|
1198
|
-
* const userService = service(UserService, async (ctx) =>
|
|
1199
|
-
* new UserService(await ctx.resolve(DatabaseService))
|
|
1200
|
-
* );
|
|
1201
|
-
* ```
|
|
783
|
+
* The type ID for the Layer interface.
|
|
784
|
+
*/
|
|
785
|
+
const LayerTypeId = Symbol.for("sandly/Layer");
|
|
786
|
+
/**
|
|
787
|
+
* Creates a layer from a builder function.
|
|
788
|
+
* @internal
|
|
789
|
+
*/
|
|
790
|
+
function createLayer(applyFn) {
|
|
791
|
+
const layerImpl = {
|
|
792
|
+
apply: applyFn,
|
|
793
|
+
provide(dependency) {
|
|
794
|
+
return createProvidedLayer(dependency, layerImpl);
|
|
795
|
+
},
|
|
796
|
+
provideMerge(dependency) {
|
|
797
|
+
return createComposedLayer(dependency, layerImpl);
|
|
798
|
+
},
|
|
799
|
+
merge(other) {
|
|
800
|
+
return createMergedLayer(layerImpl, other);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
return layerImpl;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Creates a layer that only exposes the target's provisions.
|
|
807
|
+
* @internal
|
|
1202
808
|
*/
|
|
1203
|
-
function
|
|
1204
|
-
return
|
|
1205
|
-
return container.register(tag, spec);
|
|
1206
|
-
});
|
|
809
|
+
function createProvidedLayer(dependency, target) {
|
|
810
|
+
return createComposedLayer(dependency, target);
|
|
1207
811
|
}
|
|
1208
812
|
/**
|
|
1209
|
-
* Creates a
|
|
1210
|
-
*
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
813
|
+
* Creates a composed layer that exposes both layers' provisions.
|
|
814
|
+
* @internal
|
|
815
|
+
*/
|
|
816
|
+
function createComposedLayer(dependency, target) {
|
|
817
|
+
return {
|
|
818
|
+
apply: (builder) => {
|
|
819
|
+
const withDep = dependency.apply(builder);
|
|
820
|
+
return target.apply(withDep);
|
|
821
|
+
},
|
|
822
|
+
provide(dep) {
|
|
823
|
+
return createProvidedLayer(dep, this);
|
|
824
|
+
},
|
|
825
|
+
provideMerge(dep) {
|
|
826
|
+
return createComposedLayer(dep, this);
|
|
827
|
+
},
|
|
828
|
+
merge(other) {
|
|
829
|
+
return createMergedLayer(this, other);
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Creates a merged layer from two independent layers.
|
|
835
|
+
* @internal
|
|
836
|
+
*/
|
|
837
|
+
function createMergedLayer(layer1, layer2) {
|
|
838
|
+
return {
|
|
839
|
+
apply: (builder) => {
|
|
840
|
+
const with1 = layer1.apply(builder);
|
|
841
|
+
return layer2.apply(with1);
|
|
842
|
+
},
|
|
843
|
+
provide(dep) {
|
|
844
|
+
return createProvidedLayer(dep, this);
|
|
845
|
+
},
|
|
846
|
+
provideMerge(dep) {
|
|
847
|
+
return createComposedLayer(dep, this);
|
|
848
|
+
},
|
|
849
|
+
merge(other) {
|
|
850
|
+
return createMergedLayer(this, other);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Consolidated Layer API for creating and composing dependency layers.
|
|
1220
856
|
*
|
|
1221
|
-
* @example
|
|
857
|
+
* @example
|
|
1222
858
|
* ```typescript
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
* }
|
|
1227
|
-
* connect() { return `Connected to ${this.url}`; }
|
|
859
|
+
* // Define services
|
|
860
|
+
* class Database {
|
|
861
|
+
* query(sql: string) { return []; }
|
|
1228
862
|
* }
|
|
1229
863
|
*
|
|
1230
|
-
* class UserService
|
|
1231
|
-
* constructor(private db:
|
|
1232
|
-
* super();
|
|
1233
|
-
* }
|
|
864
|
+
* class UserService {
|
|
865
|
+
* constructor(private db: Database) {}
|
|
1234
866
|
* getUsers() { return this.db.query('SELECT * FROM users'); }
|
|
1235
867
|
* }
|
|
1236
868
|
*
|
|
1237
|
-
* //
|
|
1238
|
-
* const
|
|
1239
|
-
*
|
|
869
|
+
* // Create layers
|
|
870
|
+
* const dbLayer = Layer.service(Database, []);
|
|
871
|
+
* const userLayer = Layer.service(UserService, [Database]);
|
|
1240
872
|
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
* constructor(
|
|
1245
|
-
* private logger: LoggerService,
|
|
1246
|
-
* private apiKey: string,
|
|
1247
|
-
* private retries: number,
|
|
1248
|
-
* private cache: CacheService
|
|
1249
|
-
* ) {
|
|
1250
|
-
* super();
|
|
1251
|
-
* }
|
|
1252
|
-
* }
|
|
873
|
+
* // Compose and create container
|
|
874
|
+
* const appLayer = userLayer.provide(dbLayer);
|
|
875
|
+
* const container = Container.from(appLayer);
|
|
1253
876
|
*
|
|
1254
|
-
*
|
|
1255
|
-
* const notificationService = autoService(NotificationService, [
|
|
1256
|
-
* LoggerService, // Will be resolved from container
|
|
1257
|
-
* 'secret-api-key', // Static string value
|
|
1258
|
-
* 3, // Static number value
|
|
1259
|
-
* CacheService // Will be resolved from container
|
|
1260
|
-
* ]);
|
|
1261
|
-
* ```
|
|
1262
|
-
*
|
|
1263
|
-
* @example Compared to manual service creation
|
|
1264
|
-
* ```typescript
|
|
1265
|
-
* // Manual approach (more verbose)
|
|
1266
|
-
* const userServiceManual = service(UserService, async (ctx) => {
|
|
1267
|
-
* const db = await ctx.resolve(DatabaseService);
|
|
1268
|
-
* return new UserService(db, 5000);
|
|
1269
|
-
* });
|
|
1270
|
-
*
|
|
1271
|
-
* // Auto approach (concise)
|
|
1272
|
-
* const userServiceAuto = autoService(UserService, [DatabaseService, 5000]);
|
|
1273
|
-
* ```
|
|
1274
|
-
*
|
|
1275
|
-
* @example With finalizer for cleanup
|
|
1276
|
-
* ```typescript
|
|
1277
|
-
* class DatabaseService extends Tag.Service('DatabaseService') {
|
|
1278
|
-
* constructor(private connectionString: string) {
|
|
1279
|
-
* super();
|
|
1280
|
-
* }
|
|
1281
|
-
*
|
|
1282
|
-
* private connection: Connection | null = null;
|
|
1283
|
-
*
|
|
1284
|
-
* async connect() {
|
|
1285
|
-
* this.connection = await createConnection(this.connectionString);
|
|
1286
|
-
* }
|
|
1287
|
-
*
|
|
1288
|
-
* async disconnect() {
|
|
1289
|
-
* if (this.connection) {
|
|
1290
|
-
* await this.connection.close();
|
|
1291
|
-
* this.connection = null;
|
|
1292
|
-
* }
|
|
1293
|
-
* }
|
|
1294
|
-
* }
|
|
1295
|
-
*
|
|
1296
|
-
* // Service with automatic cleanup
|
|
1297
|
-
* const dbService = autoService(
|
|
1298
|
-
* DatabaseService,
|
|
1299
|
-
* {
|
|
1300
|
-
* dependencies: ['postgresql://localhost:5432/mydb'],
|
|
1301
|
-
* cleanup: (service) => service.disconnect() // Finalizer for cleanup
|
|
1302
|
-
* }
|
|
1303
|
-
* );
|
|
877
|
+
* const users = await container.resolve(UserService);
|
|
1304
878
|
* ```
|
|
1305
879
|
*/
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
880
|
+
const Layer = {
|
|
881
|
+
service(cls, deps, options) {
|
|
882
|
+
return createLayer((builder) => {
|
|
883
|
+
return builder.add(cls, {
|
|
884
|
+
create: async (ctx) => {
|
|
885
|
+
const args = await Promise.all(deps.map((dep) => Tag.isTag(dep) ? ctx.resolve(dep) : dep));
|
|
886
|
+
return new cls(...args);
|
|
887
|
+
},
|
|
888
|
+
cleanup: options?.cleanup
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
},
|
|
892
|
+
value(tag, value) {
|
|
893
|
+
return createLayer((builder) => {
|
|
894
|
+
return builder.add(tag, () => value);
|
|
895
|
+
});
|
|
896
|
+
},
|
|
897
|
+
create(options) {
|
|
898
|
+
const layer = {
|
|
899
|
+
apply: (builder) => {
|
|
900
|
+
return options.apply(builder);
|
|
901
|
+
},
|
|
902
|
+
provide(dep) {
|
|
903
|
+
return createProvidedLayer(dep, layer);
|
|
904
|
+
},
|
|
905
|
+
provideMerge(dep) {
|
|
906
|
+
return createComposedLayer(dep, layer);
|
|
907
|
+
},
|
|
908
|
+
merge(other) {
|
|
909
|
+
return createMergedLayer(layer, other);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
return layer;
|
|
913
|
+
},
|
|
914
|
+
empty() {
|
|
915
|
+
return createLayer((builder) => builder);
|
|
916
|
+
},
|
|
917
|
+
mergeAll(...layers) {
|
|
918
|
+
return layers.reduce((acc, layer) => acc.merge(layer));
|
|
919
|
+
},
|
|
920
|
+
merge(layer1, layer2) {
|
|
921
|
+
return layer1.merge(layer2);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
1323
924
|
|
|
1324
925
|
//#endregion
|
|
1325
|
-
export { CircularDependencyError, Container,
|
|
926
|
+
export { CircularDependencyError, Container, ContainerBuilder, ContainerDestroyedError, DependencyCreationError, DependencyFinalizationError, Layer, SandlyError, ScopedContainer, ScopedContainerBuilder, Tag, UnknownDependencyError };
|