@di-framework/di-framework 0.0.0-prerelease-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.
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Dependency Injection Container
3
+ *
4
+ * Manages service registration, dependency resolution, and instance lifecycle.
5
+ * Supports singleton pattern and automatic dependency injection via decorators.
6
+ * Works with SWC and TypeScript's native decorator support.
7
+ */
8
+ const INJECTABLE_METADATA_KEY = "di:injectable";
9
+ const INJECT_METADATA_KEY = "di:inject";
10
+ const DESIGN_PARAM_TYPES_KEY = "design:paramtypes";
11
+ export const TELEMETRY_METADATA_KEY = "di:telemetry";
12
+ export const TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
13
+ /**
14
+ * Simple metadata storage that doesn't require reflect-metadata
15
+ * Works with SWC's native decorator support
16
+ */
17
+ const metadataStore = new Map();
18
+ function defineMetadata(key, value, target) {
19
+ if (!metadataStore.has(target)) {
20
+ metadataStore.set(target, new Map());
21
+ }
22
+ metadataStore.get(target).set(key, value);
23
+ }
24
+ function getMetadata(key, target) {
25
+ return metadataStore.get(target)?.get(key);
26
+ }
27
+ function hasMetadata(key, target) {
28
+ return metadataStore.has(target) && metadataStore.get(target).has(key);
29
+ }
30
+ function getOwnMetadata(key, target) {
31
+ return getMetadata(key, target);
32
+ }
33
+ export class Container {
34
+ services = new Map();
35
+ resolutionStack = new Set();
36
+ listeners = new Map();
37
+ /**
38
+ * Register a service class as injectable
39
+ */
40
+ register(serviceClass, options = { singleton: true }) {
41
+ const name = serviceClass.name;
42
+ this.services.set(name, {
43
+ type: serviceClass,
44
+ singleton: options.singleton ?? true,
45
+ });
46
+ this.services.set(serviceClass, {
47
+ type: serviceClass,
48
+ singleton: options.singleton ?? true,
49
+ });
50
+ this.emit("registered", {
51
+ key: serviceClass,
52
+ singleton: options.singleton ?? true,
53
+ kind: "class",
54
+ });
55
+ return this;
56
+ }
57
+ /**
58
+ * Register a service using a factory function
59
+ */
60
+ registerFactory(name, factory, options = { singleton: true }) {
61
+ this.services.set(name, {
62
+ type: factory,
63
+ singleton: options.singleton ?? true,
64
+ });
65
+ this.emit("registered", {
66
+ key: name,
67
+ singleton: options.singleton ?? true,
68
+ kind: "factory",
69
+ });
70
+ return this;
71
+ }
72
+ /**
73
+ * Get or create a service instance
74
+ */
75
+ resolve(serviceClass) {
76
+ const key = typeof serviceClass === "string" ? serviceClass : serviceClass;
77
+ const keyStr = typeof serviceClass === "string" ? serviceClass : serviceClass.name;
78
+ // Check for circular dependencies
79
+ if (this.resolutionStack.has(key)) {
80
+ throw new Error(`Circular dependency detected while resolving ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
81
+ }
82
+ const definition = this.services.get(key);
83
+ if (!definition) {
84
+ throw new Error(`Service '${keyStr}' is not registered in the DI container`);
85
+ }
86
+ const wasCached = definition.singleton && !!definition.instance;
87
+ // Return cached singleton
88
+ if (definition.singleton && definition.instance) {
89
+ this.emit("resolved", {
90
+ key,
91
+ instance: definition.instance,
92
+ singleton: true,
93
+ fromCache: true,
94
+ });
95
+ return definition.instance;
96
+ }
97
+ // Resolve dependencies
98
+ this.resolutionStack.add(key);
99
+ try {
100
+ const instance = this.instantiate(definition.type);
101
+ // Cache singleton
102
+ if (definition.singleton) {
103
+ definition.instance = instance;
104
+ }
105
+ this.emit("resolved", {
106
+ key,
107
+ instance,
108
+ singleton: definition.singleton,
109
+ fromCache: wasCached,
110
+ });
111
+ return instance;
112
+ }
113
+ finally {
114
+ this.resolutionStack.delete(key);
115
+ }
116
+ }
117
+ /**
118
+ * Construct a new instance without registering it in the container.
119
+ * Supports constructor overrides for primitives/config (constructor pattern).
120
+ * Always returns a fresh instance (no caching).
121
+ */
122
+ construct(serviceClass, overrides = {}) {
123
+ const keyStr = serviceClass.name;
124
+ if (this.resolutionStack.has(serviceClass)) {
125
+ throw new Error(`Circular dependency detected while constructing ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(" -> ")} -> ${keyStr}`);
126
+ }
127
+ this.resolutionStack.add(serviceClass);
128
+ try {
129
+ const instance = this.instantiate(serviceClass, overrides);
130
+ this.emit("constructed", { key: serviceClass, instance, overrides });
131
+ return instance;
132
+ }
133
+ finally {
134
+ this.resolutionStack.delete(serviceClass);
135
+ }
136
+ }
137
+ /**
138
+ * Check if a service is registered
139
+ */
140
+ has(serviceClass) {
141
+ return this.services.has(serviceClass);
142
+ }
143
+ /**
144
+ * Clear all registered services
145
+ */
146
+ clear() {
147
+ const count = this.services.size;
148
+ this.services.clear();
149
+ this.emit("cleared", { count });
150
+ }
151
+ /**
152
+ * Get all registered service names
153
+ */
154
+ getServiceNames() {
155
+ const names = new Set();
156
+ this.services.forEach((_, key) => {
157
+ if (typeof key === "string") {
158
+ names.add(key);
159
+ }
160
+ });
161
+ return Array.from(names);
162
+ }
163
+ /**
164
+ * Fork the container (prototype pattern): clone registrations into a new container.
165
+ * Optionally carry over existing singleton instances.
166
+ */
167
+ fork(options = {}) {
168
+ const clone = new Container();
169
+ this.services.forEach((def, key) => {
170
+ clone.services.set(key, {
171
+ ...def,
172
+ instance: options.carrySingletons ? def.instance : undefined,
173
+ });
174
+ });
175
+ return clone;
176
+ }
177
+ /**
178
+ * Subscribe to container lifecycle events (observer pattern).
179
+ * Returns an unsubscribe function.
180
+ */
181
+ on(event, listener) {
182
+ if (!this.listeners.has(event)) {
183
+ this.listeners.set(event, new Set());
184
+ }
185
+ this.listeners.get(event).add(listener);
186
+ return () => this.off(event, listener);
187
+ }
188
+ /**
189
+ * Remove a previously registered listener
190
+ */
191
+ off(event, listener) {
192
+ this.listeners.get(event)?.delete(listener);
193
+ }
194
+ emit(event, payload) {
195
+ const listeners = this.listeners.get(event);
196
+ if (!listeners || listeners.size === 0)
197
+ return;
198
+ listeners.forEach((listener) => {
199
+ try {
200
+ listener(payload);
201
+ }
202
+ catch (err) {
203
+ console.error(`[Container] listener for '${event}' threw`, err);
204
+ }
205
+ });
206
+ }
207
+ /**
208
+ * Private method to instantiate a service
209
+ */
210
+ instantiate(type, overrides = {}) {
211
+ if (typeof type !== "function") {
212
+ throw new Error("Service type must be a constructor or factory function");
213
+ }
214
+ // If it's a factory function (not a class), just call it
215
+ if (!this.isClass(type)) {
216
+ return type();
217
+ }
218
+ // Get constructor parameter types from metadata
219
+ const paramTypes = getMetadata(DESIGN_PARAM_TYPES_KEY, type) || [];
220
+ const paramNames = this.getConstructorParamNames(type);
221
+ // Resolve dependencies
222
+ const dependencies = [];
223
+ const injectMetadata = getOwnMetadata(INJECT_METADATA_KEY, type) || {};
224
+ const paramCount = Math.max(paramTypes.length, paramNames.length);
225
+ for (let i = 0; i < paramCount; i++) {
226
+ if (Object.prototype.hasOwnProperty.call(overrides, i)) {
227
+ dependencies.push(overrides[i]);
228
+ continue;
229
+ }
230
+ const paramType = paramTypes[i];
231
+ const paramName = paramNames[i];
232
+ // Prefer explicit @Component() decorator when present
233
+ const paramInjectTarget = injectMetadata[`param_${i}`];
234
+ if (paramInjectTarget) {
235
+ // Use explicit injection target (can be class or string token)
236
+ dependencies.push(this.resolve(paramInjectTarget));
237
+ }
238
+ else if (paramType && paramType !== Object) {
239
+ // Try to resolve by type when metadata is available
240
+ if (this.has(paramType)) {
241
+ dependencies.push(this.resolve(paramType));
242
+ }
243
+ else if (this.has(paramType.name)) {
244
+ dependencies.push(this.resolve(paramType.name));
245
+ }
246
+ else {
247
+ throw new Error(`Cannot resolve dependency of type ${paramType.name} for parameter '${paramName}' in ${type.name}`);
248
+ }
249
+ }
250
+ else {
251
+ // No information available for this parameter; leave undefined (constructor may provide default)
252
+ }
253
+ }
254
+ // Create instance
255
+ const instance = new type(...dependencies);
256
+ // Apply Telemetry and TelemetryListener
257
+ this.applyTelemetry(instance, type);
258
+ // Call @Component() decorators on properties
259
+ // Check both the instance and the constructor prototype for metadata
260
+ const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
261
+ const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) ||
262
+ {};
263
+ const allInjectProperties = {
264
+ ...injectProperties,
265
+ ...protoInjectProperties,
266
+ };
267
+ Object.entries(allInjectProperties).forEach(([propName, targetType]) => {
268
+ if (!propName.startsWith("param_") && targetType) {
269
+ try {
270
+ instance[propName] = this.resolve(targetType);
271
+ }
272
+ catch (error) {
273
+ console.warn(`Failed to inject property '${propName}' on ${type.name}:`, error);
274
+ }
275
+ }
276
+ });
277
+ return instance;
278
+ }
279
+ /**
280
+ * Apply telemetry tracking and listeners to an instance
281
+ */
282
+ applyTelemetry(instance, constructor) {
283
+ const className = constructor.name;
284
+ // Handle @TelemetryListener
285
+ const listenerMethods = getMetadata(TELEMETRY_LISTENER_METADATA_KEY, constructor.prototype) || [];
286
+ listenerMethods.forEach((methodName) => {
287
+ const method = instance[methodName];
288
+ if (typeof method === "function") {
289
+ this.on("telemetry", (payload) => {
290
+ try {
291
+ method.call(instance, payload);
292
+ }
293
+ catch (err) {
294
+ console.error(`[Container] TelemetryListener '${className}.${methodName}' threw`, err);
295
+ }
296
+ });
297
+ }
298
+ });
299
+ // Handle @Telemetry
300
+ const telemetryMethods = getMetadata(TELEMETRY_METADATA_KEY, constructor.prototype) || {};
301
+ Object.entries(telemetryMethods).forEach(([methodName, options]) => {
302
+ const originalMethod = instance[methodName];
303
+ if (typeof originalMethod === "function") {
304
+ const self = this;
305
+ instance[methodName] = function (...args) {
306
+ const startTime = Date.now();
307
+ const emit = (result, error) => {
308
+ const payload = {
309
+ className,
310
+ methodName,
311
+ args,
312
+ startTime,
313
+ endTime: Date.now(),
314
+ result,
315
+ error,
316
+ };
317
+ if (options.logging) {
318
+ const duration = payload.endTime - payload.startTime;
319
+ const status = error
320
+ ? `ERROR: ${error.message || error}`
321
+ : "SUCCESS";
322
+ console.log(`[Telemetry] ${className}.${methodName} - ${status} (${duration}ms)`);
323
+ }
324
+ self.emit("telemetry", payload);
325
+ };
326
+ try {
327
+ const result = originalMethod.apply(this, args);
328
+ if (result instanceof Promise) {
329
+ return result
330
+ .then((val) => {
331
+ emit(val);
332
+ return val;
333
+ })
334
+ .catch((err) => {
335
+ emit(undefined, err);
336
+ throw err;
337
+ });
338
+ }
339
+ emit(result);
340
+ return result;
341
+ }
342
+ catch (err) {
343
+ emit(undefined, err);
344
+ throw err;
345
+ }
346
+ };
347
+ }
348
+ });
349
+ }
350
+ /**
351
+ * Check if a function is a class constructor
352
+ */
353
+ isClass(func) {
354
+ return (typeof func === "function" &&
355
+ func.prototype &&
356
+ func.prototype.constructor === func);
357
+ }
358
+ /**
359
+ * Extract parameter names from constructor
360
+ */
361
+ getConstructorParamNames(target) {
362
+ const funcStr = target.toString();
363
+ const match = funcStr.match(/constructor\s*\(([^)]*)\)/);
364
+ if (!match || !match[1])
365
+ return [];
366
+ const paramsStr = match[1];
367
+ return paramsStr
368
+ .split(",")
369
+ .map((param) => {
370
+ const trimmed = param.trim();
371
+ const withoutDefault = trimmed.split("=")[0] || "";
372
+ const withoutType = withoutDefault.split(":")[0] || "";
373
+ return withoutType.trim();
374
+ })
375
+ .filter((param) => param);
376
+ }
377
+ /**
378
+ * Extract parameter types from TypeScript compiled code
379
+ * Looks for type annotations in the compiled constructor signature
380
+ */
381
+ extractParamTypesFromSource(target) {
382
+ const funcStr = target.toString();
383
+ // Try to extract types from decorated constructor
384
+ // In compiled TypeScript with emitDecoratorMetadata, types appear in decorator calls
385
+ const decoratorMatch = funcStr.match(/__decorate\(\[\s*(?:\w+\s*\([^)]*\),?\s*)*__param\((\d+),\s*(\w+)\([^)]*\)\)/g);
386
+ if (decoratorMatch) {
387
+ // Found decorator-based metadata
388
+ return [];
389
+ }
390
+ // Return empty array - will fall back to type annotations or @Component decorators
391
+ return [];
392
+ }
393
+ }
394
+ /**
395
+ * Global DI container instance
396
+ */
397
+ export const container = new Container();
398
+ /**
399
+ * Get the global DI container
400
+ */
401
+ export function useContainer() {
402
+ return container;
403
+ }
404
+ /**
405
+ * Export metadata functions for use in decorators
406
+ * These provide a simple, reflect-metadata-free way to store and access metadata
407
+ */
408
+ export { defineMetadata, getMetadata, hasMetadata, getOwnMetadata };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Dependency Injection Decorators
3
+ *
4
+ * @Container - Marks a class as injectable
5
+ * @Component - Marks dependencies for injection (constructor parameters or properties)
6
+ *
7
+ * Works with SWC and TypeScript's native decorator support.
8
+ * No external dependencies required (no reflect-metadata needed).
9
+ */
10
+ import { Container as DIContainer } from "./container";
11
+ /**
12
+ * Options for the @Telemetry decorator
13
+ */
14
+ export interface TelemetryOptions {
15
+ /**
16
+ * Whether to log the telemetry event to the console.
17
+ * Defaults to false.
18
+ */
19
+ logging?: boolean;
20
+ }
21
+ /**
22
+ * Marks a method for telemetry tracking.
23
+ * When called, it will emit a 'telemetry' event on the container.
24
+ * Compatible with async and sync methods.
25
+ *
26
+ * @param options Configuration options for telemetry
27
+ */
28
+ export declare function Telemetry(options?: TelemetryOptions): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
29
+ /**
30
+ * Marks a method as a listener for telemetry events.
31
+ * The method will be automatically registered to the container's 'telemetry' event.
32
+ */
33
+ export declare function TelemetryListener(): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
34
+ /**
35
+ * Marks a class as injectable and registers it with the DI container
36
+ *
37
+ * @param options Configuration options for the injectable service
38
+ *
39
+ * @example
40
+ * @Container()
41
+ * class UserService {
42
+ * getUser(id: string) { ... }
43
+ * }
44
+ *
45
+ * @example With options
46
+ * @Container({ singleton: false })
47
+ * class RequestScopedService {
48
+ * // New instance created for each resolution
49
+ * }
50
+ */
51
+ export declare function Container(options?: {
52
+ singleton?: boolean;
53
+ container?: DIContainer;
54
+ }): <T extends {
55
+ new (...args: any[]): {};
56
+ }>(constructor: T) => T;
57
+ /**
58
+ * Marks a constructor parameter or property for dependency injection
59
+ *
60
+ * Can be used on:
61
+ * - Constructor parameters
62
+ * - Class properties
63
+ *
64
+ * @param target The class to inject dependencies into. Can be a class constructor or a string identifier.
65
+ *
66
+ * @example Constructor parameter injection
67
+ * @Container()
68
+ * class UserController {
69
+ * constructor(@Component(UserService) userService: UserService) {}
70
+ * }
71
+ *
72
+ * @example Property injection
73
+ * @Container()
74
+ * class UserService {
75
+ * @Component(DatabaseConnection)
76
+ * private db: DatabaseConnection;
77
+ * }
78
+ *
79
+ * @example With string identifier
80
+ * @Container()
81
+ * class PaymentService {
82
+ * constructor(@Component('apiKey') apiKey: string) {}
83
+ * }
84
+ */
85
+ export declare function Component(target: any): (targetClass: Object | any, propertyKey?: string | symbol, parameterIndex?: number) => void;
86
+ /**
87
+ * Check if a class is marked as injectable
88
+ */
89
+ export declare function isInjectable(target: any): boolean;
90
+ /**
91
+ * Get the container instance used by decorators
92
+ */
93
+ export declare function getInjectionContainer(): DIContainer;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Dependency Injection Decorators
3
+ *
4
+ * @Container - Marks a class as injectable
5
+ * @Component - Marks dependencies for injection (constructor parameters or properties)
6
+ *
7
+ * Works with SWC and TypeScript's native decorator support.
8
+ * No external dependencies required (no reflect-metadata needed).
9
+ */
10
+ import { useContainer, Container as DIContainer, defineMetadata, getOwnMetadata, getMetadata, TELEMETRY_METADATA_KEY, TELEMETRY_LISTENER_METADATA_KEY, } from "./container";
11
+ const INJECTABLE_METADATA_KEY = "di:injectable";
12
+ const INJECT_METADATA_KEY = "di:inject";
13
+ /**
14
+ * Marks a method for telemetry tracking.
15
+ * When called, it will emit a 'telemetry' event on the container.
16
+ * Compatible with async and sync methods.
17
+ *
18
+ * @param options Configuration options for telemetry
19
+ */
20
+ export function Telemetry(options = {}) {
21
+ return function (target, propertyKey, descriptor) {
22
+ const methods = getOwnMetadata(TELEMETRY_METADATA_KEY, target) || {};
23
+ methods[propertyKey] = options;
24
+ defineMetadata(TELEMETRY_METADATA_KEY, methods, target);
25
+ };
26
+ }
27
+ /**
28
+ * Marks a method as a listener for telemetry events.
29
+ * The method will be automatically registered to the container's 'telemetry' event.
30
+ */
31
+ export function TelemetryListener() {
32
+ return function (target, propertyKey, descriptor) {
33
+ const listeners = getOwnMetadata(TELEMETRY_LISTENER_METADATA_KEY, target) || [];
34
+ listeners.push(propertyKey);
35
+ defineMetadata(TELEMETRY_LISTENER_METADATA_KEY, listeners, target);
36
+ };
37
+ }
38
+ /**
39
+ * Marks a class as injectable and registers it with the DI container
40
+ *
41
+ * @param options Configuration options for the injectable service
42
+ *
43
+ * @example
44
+ * @Container()
45
+ * class UserService {
46
+ * getUser(id: string) { ... }
47
+ * }
48
+ *
49
+ * @example With options
50
+ * @Container({ singleton: false })
51
+ * class RequestScopedService {
52
+ * // New instance created for each resolution
53
+ * }
54
+ */
55
+ export function Container(options = {}) {
56
+ return function (constructor) {
57
+ const container = options.container ?? useContainer();
58
+ const singleton = options.singleton ?? true;
59
+ // Mark as injectable using our metadata store
60
+ defineMetadata(INJECTABLE_METADATA_KEY, true, constructor);
61
+ // Register with container
62
+ container.register(constructor, { singleton });
63
+ return constructor;
64
+ };
65
+ }
66
+ /**
67
+ * Marks a constructor parameter or property for dependency injection
68
+ *
69
+ * Can be used on:
70
+ * - Constructor parameters
71
+ * - Class properties
72
+ *
73
+ * @param target The class to inject dependencies into. Can be a class constructor or a string identifier.
74
+ *
75
+ * @example Constructor parameter injection
76
+ * @Container()
77
+ * class UserController {
78
+ * constructor(@Component(UserService) userService: UserService) {}
79
+ * }
80
+ *
81
+ * @example Property injection
82
+ * @Container()
83
+ * class UserService {
84
+ * @Component(DatabaseConnection)
85
+ * private db: DatabaseConnection;
86
+ * }
87
+ *
88
+ * @example With string identifier
89
+ * @Container()
90
+ * class PaymentService {
91
+ * constructor(@Component('apiKey') apiKey: string) {}
92
+ * }
93
+ */
94
+ export function Component(target) {
95
+ return function (targetClass, propertyKey, parameterIndex) {
96
+ // Property injection
97
+ if (propertyKey && parameterIndex === undefined) {
98
+ // Store on both the class and its prototype to ensure it's accessible
99
+ const metadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass) || {};
100
+ metadata[propertyKey] = target;
101
+ defineMetadata(INJECT_METADATA_KEY, metadata, targetClass);
102
+ // Also store on the constructor if we have it
103
+ if (targetClass.constructor && targetClass.constructor !== Object) {
104
+ const constructorMetadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass.constructor) || {};
105
+ constructorMetadata[propertyKey] = target;
106
+ defineMetadata(INJECT_METADATA_KEY, constructorMetadata, targetClass.constructor);
107
+ }
108
+ }
109
+ // Constructor parameter injection
110
+ else if (parameterIndex !== undefined) {
111
+ const metadata = getOwnMetadata(INJECT_METADATA_KEY, targetClass) || {};
112
+ metadata[`param_${parameterIndex}`] = target;
113
+ defineMetadata(INJECT_METADATA_KEY, metadata, targetClass);
114
+ }
115
+ };
116
+ }
117
+ /**
118
+ * Check if a class is marked as injectable
119
+ */
120
+ export function isInjectable(target) {
121
+ return getMetadata(INJECTABLE_METADATA_KEY, target) === true;
122
+ }
123
+ /**
124
+ * Get the container instance used by decorators
125
+ */
126
+ export function getInjectionContainer() {
127
+ return useContainer();
128
+ }