sandly 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/index.d.ts +1255 -0
- package/dist/index.js +983 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
//#region src/tag.ts
|
|
4
|
+
/**
|
|
5
|
+
* Internal symbol used to identify tagged types within the dependency injection system.
|
|
6
|
+
* This symbol is used as a property key to attach metadata to both value tags and class tags.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const TagId = "__tag_id__";
|
|
10
|
+
/**
|
|
11
|
+
* Utility object containing factory functions for creating dependency tags.
|
|
12
|
+
*
|
|
13
|
+
* The Tag object provides the primary API for creating both value tags and class tags
|
|
14
|
+
* used throughout the dependency injection system. It's the main entry point for
|
|
15
|
+
* defining dependencies in a type-safe way.
|
|
16
|
+
*/
|
|
17
|
+
const Tag = {
|
|
18
|
+
of: (id) => {
|
|
19
|
+
return () => ({
|
|
20
|
+
[TagId]: id,
|
|
21
|
+
__type: void 0
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
for: () => {
|
|
25
|
+
return {
|
|
26
|
+
[TagId]: Symbol(),
|
|
27
|
+
__type: void 0
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
Class: (id) => {
|
|
31
|
+
class Tagged {
|
|
32
|
+
static [TagId] = id;
|
|
33
|
+
[TagId] = id;
|
|
34
|
+
/** @internal */
|
|
35
|
+
__type;
|
|
36
|
+
}
|
|
37
|
+
return Tagged;
|
|
38
|
+
},
|
|
39
|
+
id: (tag) => {
|
|
40
|
+
if (typeof tag === "function") {
|
|
41
|
+
const id$1 = tag[TagId];
|
|
42
|
+
return typeof id$1 === "symbol" ? id$1.toString() : String(id$1);
|
|
43
|
+
}
|
|
44
|
+
const id = tag[TagId];
|
|
45
|
+
return typeof id === "symbol" ? id.toString() : String(id);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/errors.ts
|
|
51
|
+
var BaseError = class BaseError extends Error {
|
|
52
|
+
detail;
|
|
53
|
+
constructor(message, { cause, detail } = {}) {
|
|
54
|
+
super(message, { cause });
|
|
55
|
+
this.name = this.constructor.name;
|
|
56
|
+
this.detail = detail;
|
|
57
|
+
if (cause instanceof Error && cause.stack !== void 0) this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
|
|
58
|
+
}
|
|
59
|
+
static ensure(error) {
|
|
60
|
+
return error instanceof BaseError ? error : new BaseError("An unknown error occurred", { cause: error });
|
|
61
|
+
}
|
|
62
|
+
dump() {
|
|
63
|
+
const cause = this.cause instanceof BaseError ? this.cause.dump().error : this.cause;
|
|
64
|
+
const result = {
|
|
65
|
+
name: this.name,
|
|
66
|
+
message: this.message,
|
|
67
|
+
cause,
|
|
68
|
+
detail: this.detail ?? {}
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
name: this.name,
|
|
72
|
+
message: result.message,
|
|
73
|
+
stack: this.stack,
|
|
74
|
+
error: result
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
dumps() {
|
|
78
|
+
return JSON.stringify(this.dump());
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Base error class for all dependency container related errors.
|
|
83
|
+
*
|
|
84
|
+
* This extends the framework's BaseError to provide consistent error handling
|
|
85
|
+
* and structured error information across the dependency injection system.
|
|
86
|
+
*
|
|
87
|
+
* @example Catching DI errors
|
|
88
|
+
* ```typescript
|
|
89
|
+
* try {
|
|
90
|
+
* await container.get(SomeService);
|
|
91
|
+
* } catch (error) {
|
|
92
|
+
* if (error instanceof DependencyContainerError) {
|
|
93
|
+
* console.error('DI Error:', error.message);
|
|
94
|
+
* console.error('Details:', error.detail);
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
var DependencyContainerError = class extends BaseError {};
|
|
100
|
+
/**
|
|
101
|
+
* Error thrown when attempting to retrieve a dependency that hasn't been registered.
|
|
102
|
+
*
|
|
103
|
+
* This error occurs when calling `container.get(Tag)` for a tag that was never
|
|
104
|
+
* registered via `container.register()`. It indicates a programming error where
|
|
105
|
+
* the dependency setup is incomplete.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const c = container(); // Empty container
|
|
110
|
+
*
|
|
111
|
+
* try {
|
|
112
|
+
* await c.get(UnregisteredService); // This will throw
|
|
113
|
+
* } catch (error) {
|
|
114
|
+
* if (error instanceof UnknownDependencyError) {
|
|
115
|
+
* console.error('Missing dependency:', error.message);
|
|
116
|
+
* }
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
var UnknownDependencyError = class extends DependencyContainerError {
|
|
121
|
+
/**
|
|
122
|
+
* @internal
|
|
123
|
+
* Creates an UnknownDependencyError for the given tag.
|
|
124
|
+
*
|
|
125
|
+
* @param tag - The dependency tag that wasn't found
|
|
126
|
+
*/
|
|
127
|
+
constructor(tag) {
|
|
128
|
+
super(`No factory registered for dependency ${Tag.id(tag)}`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Error thrown when a circular dependency is detected during dependency resolution.
|
|
133
|
+
*
|
|
134
|
+
* This occurs when service A depends on service B, which depends on service A (directly
|
|
135
|
+
* or through a chain of dependencies). The error includes the full dependency chain
|
|
136
|
+
* to help identify the circular reference.
|
|
137
|
+
*
|
|
138
|
+
* @example Circular dependency scenario
|
|
139
|
+
* ```typescript
|
|
140
|
+
* class ServiceA extends Tag.Class('ServiceA') {}
|
|
141
|
+
* class ServiceB extends Tag.Class('ServiceB') {}
|
|
142
|
+
*
|
|
143
|
+
* const c = container()
|
|
144
|
+
* .register(ServiceA, async (container) =>
|
|
145
|
+
* new ServiceA(await container.get(ServiceB)) // Depends on B
|
|
146
|
+
* )
|
|
147
|
+
* .register(ServiceB, async (container) =>
|
|
148
|
+
* new ServiceB(await container.get(ServiceA)) // Depends on A - CIRCULAR!
|
|
149
|
+
* );
|
|
150
|
+
*
|
|
151
|
+
* try {
|
|
152
|
+
* await c.get(ServiceA);
|
|
153
|
+
* } catch (error) {
|
|
154
|
+
* if (error instanceof CircularDependencyError) {
|
|
155
|
+
* console.error('Circular dependency:', error.message);
|
|
156
|
+
* // Output: "Circular dependency detected for ServiceA: ServiceA -> ServiceB -> ServiceA"
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
var CircularDependencyError = class extends DependencyContainerError {
|
|
162
|
+
/**
|
|
163
|
+
* @internal
|
|
164
|
+
* Creates a CircularDependencyError with the dependency chain information.
|
|
165
|
+
*
|
|
166
|
+
* @param tag - The tag where the circular dependency was detected
|
|
167
|
+
* @param dependencyChain - The chain of dependencies that led to the circular reference
|
|
168
|
+
*/
|
|
169
|
+
constructor(tag, dependencyChain) {
|
|
170
|
+
const chain = dependencyChain.map((t) => Tag.id(t)).join(" -> ");
|
|
171
|
+
super(`Circular dependency detected for ${Tag.id(tag)}: ${chain} -> ${Tag.id(tag)}`, { detail: {
|
|
172
|
+
tag: Tag.id(tag),
|
|
173
|
+
dependencyChain: dependencyChain.map((t) => Tag.id(t))
|
|
174
|
+
} });
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Error thrown when a dependency factory function throws an error during instantiation.
|
|
179
|
+
*
|
|
180
|
+
* This wraps the original error with additional context about which dependency
|
|
181
|
+
* failed to be created. The original error is preserved as the `cause` property.
|
|
182
|
+
*
|
|
183
|
+
* @example Factory throwing error
|
|
184
|
+
* ```typescript
|
|
185
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
186
|
+
*
|
|
187
|
+
* const c = container().register(DatabaseService, () => {
|
|
188
|
+
* throw new Error('Database connection failed');
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* try {
|
|
192
|
+
* await c.get(DatabaseService);
|
|
193
|
+
* } catch (error) {
|
|
194
|
+
* if (error instanceof DependencyCreationError) {
|
|
195
|
+
* console.error('Failed to create:', error.message);
|
|
196
|
+
* console.error('Original error:', error.cause);
|
|
197
|
+
* }
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
var DependencyCreationError = class extends DependencyContainerError {
|
|
202
|
+
/**
|
|
203
|
+
* @internal
|
|
204
|
+
* Creates a DependencyCreationError wrapping the original factory error.
|
|
205
|
+
*
|
|
206
|
+
* @param tag - The tag of the dependency that failed to be created
|
|
207
|
+
* @param error - The original error thrown by the factory function
|
|
208
|
+
*/
|
|
209
|
+
constructor(tag, error) {
|
|
210
|
+
super(`Error creating instance of ${Tag.id(tag)}: ${error}`, {
|
|
211
|
+
cause: error,
|
|
212
|
+
detail: { tag: Tag.id(tag) }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Error thrown when one or more finalizers fail during container destruction.
|
|
218
|
+
*
|
|
219
|
+
* This error aggregates multiple finalizer failures that occurred during
|
|
220
|
+
* `container.destroy()`. Even if some finalizers fail, the container cleanup
|
|
221
|
+
* process continues and this error contains details of all failures.
|
|
222
|
+
*
|
|
223
|
+
* @example Handling finalization errors
|
|
224
|
+
* ```typescript
|
|
225
|
+
* try {
|
|
226
|
+
* await container.destroy();
|
|
227
|
+
* } catch (error) {
|
|
228
|
+
* if (error instanceof DependencyContainerFinalizationError) {
|
|
229
|
+
* console.error('Some finalizers failed');
|
|
230
|
+
* console.error('Error details:', error.detail.errors);
|
|
231
|
+
* }
|
|
232
|
+
* }
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
var DependencyContainerFinalizationError = class extends DependencyContainerError {
|
|
236
|
+
/**
|
|
237
|
+
* @internal
|
|
238
|
+
* Creates a DependencyContainerFinalizationError aggregating multiple finalizer failures.
|
|
239
|
+
*
|
|
240
|
+
* @param errors - Array of errors thrown by individual finalizers
|
|
241
|
+
*/
|
|
242
|
+
constructor(errors) {
|
|
243
|
+
const lambdaErrors = errors.map((error) => BaseError.ensure(error));
|
|
244
|
+
super("Error destroying dependency container", {
|
|
245
|
+
cause: errors[0],
|
|
246
|
+
detail: { errors: lambdaErrors.map((error) => error.dump()) }
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/container.ts
|
|
253
|
+
/**
|
|
254
|
+
* AsyncLocalStorage instance used to track the dependency resolution chain.
|
|
255
|
+
* This enables detection of circular dependencies during async dependency resolution.
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
258
|
+
const resolutionChain = new AsyncLocalStorage();
|
|
259
|
+
/**
|
|
260
|
+
* Shared logic for dependency resolution that handles caching, circular dependency detection,
|
|
261
|
+
* and error handling. Used by both BasicDependencyContainer and ScopedDependencyContainer.
|
|
262
|
+
* @internal
|
|
263
|
+
*/
|
|
264
|
+
async function resolveDependency(tag, cache, factories, container$1) {
|
|
265
|
+
const cached = cache.get(tag);
|
|
266
|
+
if (cached !== void 0) return cached;
|
|
267
|
+
const currentChain = resolutionChain.getStore() ?? [];
|
|
268
|
+
if (currentChain.includes(tag)) throw new CircularDependencyError(tag, currentChain);
|
|
269
|
+
const factory = factories.get(tag);
|
|
270
|
+
if (factory === void 0) return Promise.reject(new UnknownDependencyError(tag));
|
|
271
|
+
const instancePromise = resolutionChain.run([...currentChain, tag], async () => {
|
|
272
|
+
try {
|
|
273
|
+
const instance = await factory(container$1);
|
|
274
|
+
return instance;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error instanceof CircularDependencyError) throw error;
|
|
277
|
+
throw new DependencyCreationError(tag, error);
|
|
278
|
+
}
|
|
279
|
+
}).catch((error) => {
|
|
280
|
+
cache.delete(tag);
|
|
281
|
+
throw error;
|
|
282
|
+
});
|
|
283
|
+
cache.set(tag, instancePromise);
|
|
284
|
+
return instancePromise;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Shared logic for running finalizers and handling cleanup errors.
|
|
288
|
+
* @internal
|
|
289
|
+
*/
|
|
290
|
+
async function runFinalizers(finalizers, cache) {
|
|
291
|
+
const promises = Array.from(finalizers.entries()).filter(([tag]) => cache.has(tag)).map(async ([tag, finalizer]) => {
|
|
292
|
+
const dep = await cache.get(tag);
|
|
293
|
+
return finalizer(dep);
|
|
294
|
+
});
|
|
295
|
+
const results = await Promise.allSettled(promises);
|
|
296
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
297
|
+
if (failures.length > 0) throw new DependencyContainerFinalizationError(failures.map((result) => result.reason));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* A type-safe dependency injection container that manages service instantiation,
|
|
301
|
+
* caching, and lifecycle management with support for async dependencies and
|
|
302
|
+
* circular dependency detection.
|
|
303
|
+
*
|
|
304
|
+
* The container maintains complete type safety by tracking registered dependencies
|
|
305
|
+
* at the type level, ensuring that only registered dependencies can be retrieved
|
|
306
|
+
* and preventing runtime errors.
|
|
307
|
+
*
|
|
308
|
+
* @template TReg - Union type of all registered dependency tags in this container
|
|
309
|
+
*
|
|
310
|
+
* @example Basic usage with class tags
|
|
311
|
+
* ```typescript
|
|
312
|
+
* import { container, Tag } from 'sandl';
|
|
313
|
+
*
|
|
314
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
315
|
+
* query() { return 'data'; }
|
|
316
|
+
* }
|
|
317
|
+
*
|
|
318
|
+
* class UserService extends Tag.Class('UserService') {
|
|
319
|
+
* constructor(private db: DatabaseService) {}
|
|
320
|
+
* getUser() { return this.db.query(); }
|
|
321
|
+
* }
|
|
322
|
+
*
|
|
323
|
+
* const c = container()
|
|
324
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
325
|
+
* .register(UserService, async (container) =>
|
|
326
|
+
* new UserService(await container.get(DatabaseService))
|
|
327
|
+
* );
|
|
328
|
+
*
|
|
329
|
+
* const userService = await c.get(UserService);
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @example Usage with value tags
|
|
333
|
+
* ```typescript
|
|
334
|
+
* const ApiKeyTag = Tag.of('apiKey')<string>();
|
|
335
|
+
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
336
|
+
*
|
|
337
|
+
* const c = container()
|
|
338
|
+
* .register(ApiKeyTag, () => process.env.API_KEY!)
|
|
339
|
+
* .register(ConfigTag, () => ({ dbUrl: 'postgresql://localhost:5432' }));
|
|
340
|
+
*
|
|
341
|
+
* const apiKey = await c.get(ApiKeyTag);
|
|
342
|
+
* const config = await c.get(ConfigTag);
|
|
343
|
+
* ```
|
|
344
|
+
*
|
|
345
|
+
* @example With finalizers for cleanup
|
|
346
|
+
* ```typescript
|
|
347
|
+
* class DatabaseConnection extends Tag.Class('DatabaseConnection') {
|
|
348
|
+
* async connect() { return; }
|
|
349
|
+
* async disconnect() { return; }
|
|
350
|
+
* }
|
|
351
|
+
*
|
|
352
|
+
* const c = container().register(
|
|
353
|
+
* DatabaseConnection,
|
|
354
|
+
* async () => {
|
|
355
|
+
* const conn = new DatabaseConnection();
|
|
356
|
+
* await conn.connect();
|
|
357
|
+
* return conn;
|
|
358
|
+
* },
|
|
359
|
+
* async (conn) => conn.disconnect() // Finalizer for cleanup
|
|
360
|
+
* );
|
|
361
|
+
*
|
|
362
|
+
* // Later...
|
|
363
|
+
* await c.destroy(); // Calls all finalizers
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
var Container = class {
|
|
367
|
+
/**
|
|
368
|
+
* Cache of instantiated dependencies as promises.
|
|
369
|
+
* Ensures singleton behavior and supports concurrent access.
|
|
370
|
+
* @internal
|
|
371
|
+
*/
|
|
372
|
+
cache = /* @__PURE__ */ new Map();
|
|
373
|
+
/**
|
|
374
|
+
* Factory functions for creating dependency instances.
|
|
375
|
+
* @internal
|
|
376
|
+
*/
|
|
377
|
+
factories = /* @__PURE__ */ new Map();
|
|
378
|
+
/**
|
|
379
|
+
* Finalizer functions for cleaning up dependencies when the container is destroyed.
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
382
|
+
finalizers = /* @__PURE__ */ new Map();
|
|
383
|
+
/**
|
|
384
|
+
* Registers a dependency in the container with a factory function and optional finalizer.
|
|
385
|
+
*
|
|
386
|
+
* The factory function receives the current container instance and must return the
|
|
387
|
+
* service instance (or a Promise of it). The container tracks the registration at
|
|
388
|
+
* the type level, ensuring type safety for subsequent `.get()` calls.
|
|
389
|
+
*
|
|
390
|
+
* @template T - The dependency tag being registered
|
|
391
|
+
* @param tag - The dependency tag (class or value tag)
|
|
392
|
+
* @param factory - Function that creates the service instance, receives container for dependency injection
|
|
393
|
+
* @param finalizer - Optional cleanup function called when container is destroyed
|
|
394
|
+
* @returns A new container instance with the dependency registered
|
|
395
|
+
* @throws {DependencyContainerError} If the dependency is already registered
|
|
396
|
+
*
|
|
397
|
+
* @example Registering a simple service
|
|
398
|
+
* ```typescript
|
|
399
|
+
* class LoggerService extends Tag.Class('LoggerService') {
|
|
400
|
+
* log(message: string) { console.log(message); }
|
|
401
|
+
* }
|
|
402
|
+
*
|
|
403
|
+
* const c = container().register(
|
|
404
|
+
* LoggerService,
|
|
405
|
+
* () => new LoggerService()
|
|
406
|
+
* );
|
|
407
|
+
* ```
|
|
408
|
+
*
|
|
409
|
+
* @example Registering with dependencies
|
|
410
|
+
* ```typescript
|
|
411
|
+
* class UserService extends Tag.Class('UserService') {
|
|
412
|
+
* constructor(private db: DatabaseService, private logger: LoggerService) {}
|
|
413
|
+
* }
|
|
414
|
+
*
|
|
415
|
+
* const c = container()
|
|
416
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
417
|
+
* .register(LoggerService, () => new LoggerService())
|
|
418
|
+
* .register(UserService, async (container) =>
|
|
419
|
+
* new UserService(
|
|
420
|
+
* await container.get(DatabaseService),
|
|
421
|
+
* await container.get(LoggerService)
|
|
422
|
+
* )
|
|
423
|
+
* );
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* @example Using value tags
|
|
427
|
+
* ```typescript
|
|
428
|
+
* const ConfigTag = Tag.of('config')<{ apiUrl: string }>();
|
|
429
|
+
*
|
|
430
|
+
* const c = container().register(
|
|
431
|
+
* ConfigTag,
|
|
432
|
+
* () => ({ apiUrl: 'https://api.example.com' })
|
|
433
|
+
* );
|
|
434
|
+
* ```
|
|
435
|
+
*
|
|
436
|
+
* @example With finalizer for cleanup
|
|
437
|
+
* ```typescript
|
|
438
|
+
* class DatabaseConnection extends Tag.Class('DatabaseConnection') {
|
|
439
|
+
* async connect() { return; }
|
|
440
|
+
* async close() { return; }
|
|
441
|
+
* }
|
|
442
|
+
*
|
|
443
|
+
* const c = container().register(
|
|
444
|
+
* DatabaseConnection,
|
|
445
|
+
* async () => {
|
|
446
|
+
* const conn = new DatabaseConnection();
|
|
447
|
+
* await conn.connect();
|
|
448
|
+
* return conn;
|
|
449
|
+
* },
|
|
450
|
+
* (conn) => conn.close() // Called during container.destroy()
|
|
451
|
+
* );
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
register(tag, factoryOrLifecycle) {
|
|
455
|
+
if (this.factories.has(tag)) throw new DependencyContainerError(`Dependency ${Tag.id(tag)} already registered`);
|
|
456
|
+
if (typeof factoryOrLifecycle === "function") this.factories.set(tag, factoryOrLifecycle);
|
|
457
|
+
else {
|
|
458
|
+
this.factories.set(tag, factoryOrLifecycle.factory);
|
|
459
|
+
this.finalizers.set(tag, factoryOrLifecycle.finalizer);
|
|
460
|
+
}
|
|
461
|
+
return this;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Checks if a dependency has been instantiated (cached) in the container.
|
|
465
|
+
*
|
|
466
|
+
* Note: This returns `true` only after the dependency has been created via `.get()`.
|
|
467
|
+
* A registered but not-yet-instantiated dependency will return `false`.
|
|
468
|
+
*
|
|
469
|
+
* @param tag - The dependency tag to check
|
|
470
|
+
* @returns `true` if the dependency has been instantiated and cached, `false` otherwise
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* const c = container().register(DatabaseService, () => new DatabaseService());
|
|
475
|
+
*
|
|
476
|
+
* console.log(c.has(DatabaseService)); // false - not instantiated yet
|
|
477
|
+
*
|
|
478
|
+
* await c.get(DatabaseService);
|
|
479
|
+
* console.log(c.has(DatabaseService)); // true - now instantiated and cached
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
has(tag) {
|
|
483
|
+
return this.cache.has(tag);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Retrieves a dependency instance from the container, creating it if necessary.
|
|
487
|
+
*
|
|
488
|
+
* This method ensures singleton behavior - each dependency is created only once
|
|
489
|
+
* and cached for subsequent calls. The method is async-safe and handles concurrent
|
|
490
|
+
* requests for the same dependency correctly.
|
|
491
|
+
*
|
|
492
|
+
* The method performs circular dependency detection using AsyncLocalStorage to track
|
|
493
|
+
* the resolution chain across async boundaries.
|
|
494
|
+
*
|
|
495
|
+
* @template T - The dependency tag type (must be registered in this container)
|
|
496
|
+
* @param tag - The dependency tag to retrieve
|
|
497
|
+
* @returns Promise resolving to the service instance
|
|
498
|
+
* @throws {UnknownDependencyError} If the dependency is not registered
|
|
499
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
500
|
+
* @throws {DependencyCreationError} If the factory function throws an error
|
|
501
|
+
*
|
|
502
|
+
* @example Basic usage
|
|
503
|
+
* ```typescript
|
|
504
|
+
* const c = container()
|
|
505
|
+
* .register(DatabaseService, () => new DatabaseService());
|
|
506
|
+
*
|
|
507
|
+
* const db = await c.get(DatabaseService);
|
|
508
|
+
* db.query('SELECT * FROM users');
|
|
509
|
+
* ```
|
|
510
|
+
*
|
|
511
|
+
* @example Concurrent access (singleton behavior)
|
|
512
|
+
* ```typescript
|
|
513
|
+
* // All three calls will receive the same instance
|
|
514
|
+
* const [db1, db2, db3] = await Promise.all([
|
|
515
|
+
* c.get(DatabaseService),
|
|
516
|
+
* c.get(DatabaseService),
|
|
517
|
+
* c.get(DatabaseService)
|
|
518
|
+
* ]);
|
|
519
|
+
*
|
|
520
|
+
* console.log(db1 === db2 === db3); // true
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* @example Dependency injection in factories
|
|
524
|
+
* ```typescript
|
|
525
|
+
* const c = container()
|
|
526
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
527
|
+
* .register(UserService, async (container) => {
|
|
528
|
+
* const db = await container.get(DatabaseService);
|
|
529
|
+
* return new UserService(db);
|
|
530
|
+
* });
|
|
531
|
+
*
|
|
532
|
+
* const userService = await c.get(UserService);
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
async get(tag) {
|
|
536
|
+
return resolveDependency(tag, this.cache, this.factories, this);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Destroys all instantiated dependencies by calling their finalizers, then clears the instance cache.
|
|
540
|
+
*
|
|
541
|
+
* **Important: This method preserves the container structure (factories and finalizers) for reuse.**
|
|
542
|
+
* The container can be used again after destruction to create fresh instances following the same
|
|
543
|
+
* dependency patterns.
|
|
544
|
+
*
|
|
545
|
+
* All finalizers for instantiated dependencies are called concurrently using Promise.allSettled()
|
|
546
|
+
* for maximum cleanup performance.
|
|
547
|
+
* If any finalizers fail, all errors are collected and a DependencyContainerFinalizationError
|
|
548
|
+
* is thrown containing details of all failures.
|
|
549
|
+
*
|
|
550
|
+
* **Finalizer Concurrency:** Finalizers run concurrently, so there are no ordering guarantees.
|
|
551
|
+
* Services should be designed to handle cleanup gracefully regardless of the order in which their
|
|
552
|
+
* dependencies are cleaned up.
|
|
553
|
+
*
|
|
554
|
+
* @returns Promise that resolves when all cleanup is complete
|
|
555
|
+
* @throws {DependencyContainerFinalizationError} If any finalizers fail during cleanup
|
|
556
|
+
*
|
|
557
|
+
* @example Basic cleanup and reuse
|
|
558
|
+
* ```typescript
|
|
559
|
+
* const c = container()
|
|
560
|
+
* .register(DatabaseConnection,
|
|
561
|
+
* async () => {
|
|
562
|
+
* const conn = new DatabaseConnection();
|
|
563
|
+
* await conn.connect();
|
|
564
|
+
* return conn;
|
|
565
|
+
* },
|
|
566
|
+
* (conn) => conn.disconnect() // Finalizer
|
|
567
|
+
* );
|
|
568
|
+
*
|
|
569
|
+
* // First use cycle
|
|
570
|
+
* const db1 = await c.get(DatabaseConnection);
|
|
571
|
+
* await c.destroy(); // Calls conn.disconnect(), clears cache
|
|
572
|
+
*
|
|
573
|
+
* // Container can be reused - creates fresh instances
|
|
574
|
+
* const db2 = await c.get(DatabaseConnection); // New connection
|
|
575
|
+
* expect(db2).not.toBe(db1); // Different instances
|
|
576
|
+
* ```
|
|
577
|
+
*
|
|
578
|
+
* @example Multiple destroy/reuse cycles
|
|
579
|
+
* ```typescript
|
|
580
|
+
* const c = container().register(UserService, () => new UserService());
|
|
581
|
+
*
|
|
582
|
+
* for (let i = 0; i < 5; i++) {
|
|
583
|
+
* const user = await c.get(UserService);
|
|
584
|
+
* // ... use service ...
|
|
585
|
+
* await c.destroy(); // Clean up, ready for next cycle
|
|
586
|
+
* }
|
|
587
|
+
* ```
|
|
588
|
+
*
|
|
589
|
+
* @example Handling cleanup errors
|
|
590
|
+
* ```typescript
|
|
591
|
+
* try {
|
|
592
|
+
* await container.destroy();
|
|
593
|
+
* } catch (error) {
|
|
594
|
+
* if (error instanceof DependencyContainerFinalizationError) {
|
|
595
|
+
* console.error('Some dependencies failed to clean up:', error.detail.errors);
|
|
596
|
+
* }
|
|
597
|
+
* }
|
|
598
|
+
* // Container is still reusable even after finalizer errors
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
async destroy() {
|
|
602
|
+
try {
|
|
603
|
+
await runFinalizers(this.finalizers, this.cache);
|
|
604
|
+
} finally {
|
|
605
|
+
this.cache.clear();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
var ScopedContainer = class ScopedContainer {
|
|
610
|
+
scope;
|
|
611
|
+
parent;
|
|
612
|
+
children = [];
|
|
613
|
+
/**
|
|
614
|
+
* Cache of instantiated dependencies as promises for this scope.
|
|
615
|
+
* @internal
|
|
616
|
+
*/
|
|
617
|
+
cache = /* @__PURE__ */ new Map();
|
|
618
|
+
/**
|
|
619
|
+
* Factory functions for creating dependency instances in this scope.
|
|
620
|
+
* @internal
|
|
621
|
+
*/
|
|
622
|
+
factories = /* @__PURE__ */ new Map();
|
|
623
|
+
/**
|
|
624
|
+
* Finalizer functions for cleaning up dependencies when this scope is destroyed.
|
|
625
|
+
* @internal
|
|
626
|
+
*/
|
|
627
|
+
finalizers = /* @__PURE__ */ new Map();
|
|
628
|
+
constructor(parent, scope) {
|
|
629
|
+
this.parent = parent;
|
|
630
|
+
this.scope = scope;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Registers a dependency in the specified scope within this container's scope chain.
|
|
634
|
+
*
|
|
635
|
+
* If no scope is specified, registers in the current (leaf) scope. If a scope is specified,
|
|
636
|
+
* delegates to the parent container if the target scope doesn't match the current scope.
|
|
637
|
+
*
|
|
638
|
+
* This allows registering dependencies at different scope levels from any container
|
|
639
|
+
* in the scope chain, providing flexibility for dependency organization.
|
|
640
|
+
*
|
|
641
|
+
* @param tag - The dependency tag to register
|
|
642
|
+
* @param factory - Factory function to create the dependency
|
|
643
|
+
* @param finalizer - Optional cleanup function
|
|
644
|
+
* @param scope - Target scope for registration (defaults to current scope)
|
|
645
|
+
* @returns This container with updated type information
|
|
646
|
+
*
|
|
647
|
+
* @example Registering in different scopes
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const runtime = scopedContainer('runtime');
|
|
650
|
+
* const request = runtime.child('request');
|
|
651
|
+
*
|
|
652
|
+
* // Register in current (request) scope
|
|
653
|
+
* request.register(RequestService, () => new RequestService());
|
|
654
|
+
*
|
|
655
|
+
* // Register in runtime scope from request container - delegates to parent
|
|
656
|
+
* request.register(DatabaseService, () => new DatabaseService(), undefined, 'runtime');
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
register(tag, factoryOrLifecycle, scope) {
|
|
660
|
+
if (scope === void 0 || scope === this.scope) {
|
|
661
|
+
if (this.factories.has(tag)) throw new DependencyContainerError(`Dependency ${Tag.id(tag)} already registered in scope '${String(this.scope)}'`);
|
|
662
|
+
if (typeof factoryOrLifecycle === "function") this.factories.set(tag, factoryOrLifecycle);
|
|
663
|
+
else {
|
|
664
|
+
this.factories.set(tag, factoryOrLifecycle.factory);
|
|
665
|
+
this.finalizers.set(tag, factoryOrLifecycle.finalizer);
|
|
666
|
+
}
|
|
667
|
+
return this;
|
|
668
|
+
}
|
|
669
|
+
if (this.parent === null) throw new DependencyContainerError(`Scope '${String(scope)}' not found in container chain`);
|
|
670
|
+
this.parent.register(tag, factoryOrLifecycle, scope);
|
|
671
|
+
return this;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Checks if a dependency has been instantiated in this scope or any parent scope.
|
|
675
|
+
*
|
|
676
|
+
* This method checks the current scope first, then walks up the parent chain.
|
|
677
|
+
* Returns true only if the dependency has been created and cached somewhere in the scope hierarchy.
|
|
678
|
+
*/
|
|
679
|
+
has(tag) {
|
|
680
|
+
if (this.cache.has(tag)) return true;
|
|
681
|
+
return this.parent?.has(tag) ?? false;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Retrieves a dependency instance, resolving from the current scope or parent scopes.
|
|
685
|
+
*
|
|
686
|
+
* Resolution strategy:
|
|
687
|
+
* 1. Check cache in current scope
|
|
688
|
+
* 2. Check if factory exists in current scope - if so, create instance here
|
|
689
|
+
* 3. Otherwise, delegate to parent scope
|
|
690
|
+
* 4. If no parent or parent doesn't have it, throw UnknownDependencyError
|
|
691
|
+
*/
|
|
692
|
+
async get(tag) {
|
|
693
|
+
if (this.factories.has(tag)) return resolveDependency(tag, this.cache, this.factories, this);
|
|
694
|
+
if (this.parent !== null) return this.parent.get(tag);
|
|
695
|
+
throw new UnknownDependencyError(tag);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Destroys this scoped container and its children, preserving the container structure for reuse.
|
|
699
|
+
*
|
|
700
|
+
* This method ensures proper cleanup order while maintaining reusability:
|
|
701
|
+
* 1. Destroys all child scopes first (they may depend on parent scope dependencies)
|
|
702
|
+
* 2. Then calls finalizers for dependencies created in this scope
|
|
703
|
+
* 3. Clears only instance caches - preserves factories, finalizers, and child structure
|
|
704
|
+
*
|
|
705
|
+
* Child destruction happens first to ensure dependencies don't get cleaned up
|
|
706
|
+
* before their dependents.
|
|
707
|
+
*/
|
|
708
|
+
async destroy() {
|
|
709
|
+
const allFailures = [];
|
|
710
|
+
try {
|
|
711
|
+
const childDestroyPromises = this.children.map((child) => child.destroy());
|
|
712
|
+
const childResults = await Promise.allSettled(childDestroyPromises);
|
|
713
|
+
const childFailures = childResults.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
714
|
+
allFailures.push(...childFailures);
|
|
715
|
+
await runFinalizers(this.finalizers, this.cache);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
allFailures.push(error);
|
|
718
|
+
} finally {
|
|
719
|
+
this.cache.clear();
|
|
720
|
+
}
|
|
721
|
+
if (allFailures.length > 0) throw new DependencyContainerFinalizationError(allFailures);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Creates a child scoped container.
|
|
725
|
+
*
|
|
726
|
+
* Child containers inherit access to parent dependencies but maintain
|
|
727
|
+
* their own scope for new registrations and instance caching.
|
|
728
|
+
*/
|
|
729
|
+
child(scope) {
|
|
730
|
+
const child = new ScopedContainer(this, scope);
|
|
731
|
+
this.children.push(child);
|
|
732
|
+
return child;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* Creates a new empty dependency injection container.
|
|
737
|
+
*
|
|
738
|
+
* This is a convenience factory function that creates a new DependencyContainer instance.
|
|
739
|
+
* The returned container starts with no registered dependencies and the type parameter
|
|
740
|
+
* defaults to `never`, indicating no dependencies are available for retrieval yet.
|
|
741
|
+
*
|
|
742
|
+
* @returns A new empty DependencyContainer instance
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```typescript
|
|
746
|
+
* import { container, Tag } from 'sandl';
|
|
747
|
+
*
|
|
748
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {}
|
|
749
|
+
* class UserService extends Tag.Class('UserService') {}
|
|
750
|
+
*
|
|
751
|
+
* const c = container()
|
|
752
|
+
* .register(DatabaseService, () => new DatabaseService())
|
|
753
|
+
* .register(UserService, async (container) =>
|
|
754
|
+
* new UserService(await container.get(DatabaseService))
|
|
755
|
+
* );
|
|
756
|
+
*
|
|
757
|
+
* const userService = await c.get(UserService);
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
function container() {
|
|
761
|
+
return new Container();
|
|
762
|
+
}
|
|
763
|
+
function scopedContainer(scope) {
|
|
764
|
+
return new ScopedContainer(null, scope);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/layer.ts
|
|
769
|
+
/**
|
|
770
|
+
* Creates a new dependency layer that encapsulates a set of dependency registrations.
|
|
771
|
+
* Layers are the primary building blocks for organizing and composing dependency injection setups.
|
|
772
|
+
*
|
|
773
|
+
* @template TRequires - The union of dependency tags this layer requires from other layers or external setup
|
|
774
|
+
* @template TProvides - The union of dependency tags this layer registers/provides
|
|
775
|
+
* @template TParams - Optional parameters that can be passed to configure the layer
|
|
776
|
+
*
|
|
777
|
+
* @param register - Function that performs the dependency registrations. Receives a container and optional params.
|
|
778
|
+
* @returns A layer factory function. If TParams is undefined, returns a parameterless function. Otherwise returns a function that takes TParams.
|
|
779
|
+
*
|
|
780
|
+
* @example Simple layer without parameters
|
|
781
|
+
* ```typescript
|
|
782
|
+
* import { layer, Tag } from '@/di/layer.js';
|
|
783
|
+
*
|
|
784
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
785
|
+
* constructor(private url: string = 'sqlite://memory') {}
|
|
786
|
+
* query() { return 'data'; }
|
|
787
|
+
* }
|
|
788
|
+
*
|
|
789
|
+
* // Layer that provides DatabaseService, requires nothing
|
|
790
|
+
* const databaseLayer = layer<never, typeof DatabaseService>((container) =>
|
|
791
|
+
* container.register(DatabaseService, () => new DatabaseService())
|
|
792
|
+
* );
|
|
793
|
+
*
|
|
794
|
+
* // Usage
|
|
795
|
+
* const dbLayerInstance = databaseLayer(); // No parameters needed
|
|
796
|
+
* ```
|
|
797
|
+
*
|
|
798
|
+
* @example Layer with dependencies
|
|
799
|
+
* ```typescript
|
|
800
|
+
* const ConfigTag = Tag.of('config')<{ dbUrl: string }>();
|
|
801
|
+
*
|
|
802
|
+
* // Layer that requires ConfigTag and provides DatabaseService
|
|
803
|
+
* const databaseLayer = layer<typeof ConfigTag, typeof DatabaseService>((container) =>
|
|
804
|
+
* container.register(DatabaseService, async (c) => {
|
|
805
|
+
* const config = await c.get(ConfigTag);
|
|
806
|
+
* return new DatabaseService(config.dbUrl);
|
|
807
|
+
* })
|
|
808
|
+
* );
|
|
809
|
+
* ```
|
|
810
|
+
*
|
|
811
|
+
* @example Parameterized layer
|
|
812
|
+
* ```typescript
|
|
813
|
+
* interface DatabaseConfig {
|
|
814
|
+
* host: string;
|
|
815
|
+
* port: number;
|
|
816
|
+
* }
|
|
817
|
+
*
|
|
818
|
+
* // Layer that takes configuration parameters
|
|
819
|
+
* const databaseLayer = layer<never, typeof DatabaseService, DatabaseConfig>(
|
|
820
|
+
* (container, config) =>
|
|
821
|
+
* container.register(DatabaseService, () => new DatabaseService(config))
|
|
822
|
+
* );
|
|
823
|
+
*
|
|
824
|
+
* // Usage with parameters
|
|
825
|
+
* const dbLayerInstance = databaseLayer({ host: 'localhost', port: 5432 });
|
|
826
|
+
* ```
|
|
827
|
+
*
|
|
828
|
+
* @example Complex application layer structure
|
|
829
|
+
* ```typescript
|
|
830
|
+
* // Configuration layer
|
|
831
|
+
* const configLayer = layer<never, typeof ConfigTag>((container) =>
|
|
832
|
+
* container.register(ConfigTag, () => loadConfig())
|
|
833
|
+
* );
|
|
834
|
+
*
|
|
835
|
+
* // Infrastructure layer (requires config)
|
|
836
|
+
* const infraLayer = layer<typeof ConfigTag, typeof DatabaseService | typeof CacheService>(
|
|
837
|
+
* (container) =>
|
|
838
|
+
* container
|
|
839
|
+
* .register(DatabaseService, async (c) => new DatabaseService(await c.get(ConfigTag)))
|
|
840
|
+
* .register(CacheService, async (c) => new CacheService(await c.get(ConfigTag)))
|
|
841
|
+
* );
|
|
842
|
+
*
|
|
843
|
+
* // Service layer (requires infrastructure)
|
|
844
|
+
* const serviceLayer = layer<typeof DatabaseService | typeof CacheService, typeof UserService>(
|
|
845
|
+
* (container) =>
|
|
846
|
+
* container.register(UserService, async (c) =>
|
|
847
|
+
* new UserService(await c.get(DatabaseService), await c.get(CacheService))
|
|
848
|
+
* )
|
|
849
|
+
* );
|
|
850
|
+
*
|
|
851
|
+
* // Compose the complete application
|
|
852
|
+
* const appLayer = configLayer().to(infraLayer()).to(serviceLayer());
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
function layer(register) {
|
|
856
|
+
const factory = (params) => {
|
|
857
|
+
const layerImpl = {
|
|
858
|
+
register: (container$1) => register(container$1, params),
|
|
859
|
+
to(target) {
|
|
860
|
+
return createComposedLayer(layerImpl, target);
|
|
861
|
+
},
|
|
862
|
+
and(other) {
|
|
863
|
+
return createMergedLayer(layerImpl, other);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
return layerImpl;
|
|
867
|
+
};
|
|
868
|
+
return factory;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Internal function to create a composed layer from two layers.
|
|
872
|
+
* This implements the `.to()` method logic.
|
|
873
|
+
*
|
|
874
|
+
* @internal
|
|
875
|
+
*/
|
|
876
|
+
function createComposedLayer(source, target) {
|
|
877
|
+
return layer((container$1) => {
|
|
878
|
+
const containerWithSource = source.register(container$1);
|
|
879
|
+
return target.register(containerWithSource);
|
|
880
|
+
})();
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Internal function to create a merged layer from two layers.
|
|
884
|
+
* This implements the `.and()` method logic.
|
|
885
|
+
*
|
|
886
|
+
* @internal
|
|
887
|
+
*/
|
|
888
|
+
function createMergedLayer(layer1, layer2) {
|
|
889
|
+
return layer((container$1) => {
|
|
890
|
+
const container1 = layer1.register(container$1);
|
|
891
|
+
return layer2.register(container1);
|
|
892
|
+
})();
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Utility object containing helper functions for working with layers.
|
|
896
|
+
*/
|
|
897
|
+
const Layer = {
|
|
898
|
+
empty() {
|
|
899
|
+
return layer((container$1) => container$1)();
|
|
900
|
+
},
|
|
901
|
+
merge(...layers) {
|
|
902
|
+
return layers.reduce((acc, layer$1) => acc.and(layer$1));
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/service.ts
|
|
908
|
+
/**
|
|
909
|
+
* Creates a service layer from any tag type (ClassTag or ValueTag) with optional parameters.
|
|
910
|
+
*
|
|
911
|
+
* For ClassTag services:
|
|
912
|
+
* - Dependencies are automatically inferred from constructor parameters
|
|
913
|
+
* - The factory function must handle dependency injection by resolving dependencies from the container
|
|
914
|
+
*
|
|
915
|
+
* For ValueTag services:
|
|
916
|
+
* - No constructor dependencies are needed since they don't have constructors
|
|
917
|
+
*
|
|
918
|
+
* @template T - The tag representing the service (ClassTag or ValueTag)
|
|
919
|
+
* @template TParams - Optional parameters for service configuration
|
|
920
|
+
* @param serviceClass - The tag (ClassTag or ValueTag)
|
|
921
|
+
* @param factory - Factory function for service instantiation with container and optional params
|
|
922
|
+
* @returns A factory function that creates a service layer
|
|
923
|
+
*
|
|
924
|
+
* @example Simple service without dependencies
|
|
925
|
+
* ```typescript
|
|
926
|
+
* class LoggerService extends Tag.Class('LoggerService') {
|
|
927
|
+
* log(message: string) { console.log(message); }
|
|
928
|
+
* }
|
|
929
|
+
*
|
|
930
|
+
* const loggerService = service(LoggerService, () => new LoggerService());
|
|
931
|
+
* ```
|
|
932
|
+
*
|
|
933
|
+
* @example Service with dependencies
|
|
934
|
+
* ```typescript
|
|
935
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
936
|
+
* query() { return []; }
|
|
937
|
+
* }
|
|
938
|
+
*
|
|
939
|
+
* class UserService extends Tag.Class('UserService') {
|
|
940
|
+
* constructor(private db: DatabaseService) {
|
|
941
|
+
* super();
|
|
942
|
+
* }
|
|
943
|
+
*
|
|
944
|
+
* getUsers() { return this.db.query(); }
|
|
945
|
+
* }
|
|
946
|
+
*
|
|
947
|
+
* const userService = service(UserService, async (container) =>
|
|
948
|
+
* new UserService(await container.get(DatabaseService))
|
|
949
|
+
* );
|
|
950
|
+
* ```
|
|
951
|
+
*
|
|
952
|
+
* @example Service with configuration parameters
|
|
953
|
+
* ```typescript
|
|
954
|
+
* class DatabaseService extends Tag.Class('DatabaseService') {
|
|
955
|
+
* constructor(private config: { dbUrl: string }) {
|
|
956
|
+
* super();
|
|
957
|
+
* }
|
|
958
|
+
* }
|
|
959
|
+
*
|
|
960
|
+
* const dbService = service(
|
|
961
|
+
* DatabaseService,
|
|
962
|
+
* (container, params: { dbUrl: string }) => new DatabaseService(params)
|
|
963
|
+
* );
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
function service(serviceClass, factory) {
|
|
967
|
+
const serviceFactory = (params) => {
|
|
968
|
+
const serviceLayer = layer((container$1) => {
|
|
969
|
+
return container$1.register(serviceClass, (c) => factory(c, params));
|
|
970
|
+
})();
|
|
971
|
+
const serviceImpl = {
|
|
972
|
+
serviceClass,
|
|
973
|
+
register: serviceLayer.register,
|
|
974
|
+
to: serviceLayer.to,
|
|
975
|
+
and: serviceLayer.and
|
|
976
|
+
};
|
|
977
|
+
return serviceImpl;
|
|
978
|
+
};
|
|
979
|
+
return serviceFactory;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
//#endregion
|
|
983
|
+
export { Layer, Tag, container, layer, scopedContainer, service };
|