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