-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
379 lines (369 loc) · 10.3 KB
/
index.ts
File metadata and controls
379 lines (369 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import type { Token } from "../types/index.js";
import { Lifetime, METADATA_KEYS } from "../types/index.js";
// Polyfill for reflect-metadata methods
const metadataStore = new WeakMap<Object, Map<string | symbol, any>>();
function defineMetadata(key: string | symbol, value: any, target: Object): void {
if (!metadataStore.has(target)) {
metadataStore.set(target, new Map());
}
metadataStore.get(target)!.set(key, value);
}
function getMetadata(key: string | symbol, target: Object): any {
return metadataStore.get(target)?.get(key);
}
/**
* Get decorator metadata (exported for use by Registry)
*/
export function getDecoratorMetadata(key: string | symbol, target: Object): any {
return getMetadata(key, target);
}
/**
* Clear decorator metadata for a target class.
*
* This function removes all decorator metadata associated with a class.
* Useful for cleaning up dynamically created classes to prevent memory leaks,
* as the inner Map in the WeakMap metadata store won't be garbage collected
* automatically even after the class reference is lost.
*
* @param target - The class to clear metadata for
*
* @example
* ```typescript
* @singleton()
* class DynamicService {}
*
* // Later, when the class is no longer needed
* clearDecoratorMetadata(DynamicService);
* ```
*
* @example
* ```typescript
* // In a test cleanup
* afterEach(() => {
* clearDecoratorMetadata(TestService);
* });
* ```
*/
export function clearDecoratorMetadata(target: Object): void {
metadataStore.delete(target);
}
/**
* Mark a class as injectable and eligible for dependency injection.
*
* This decorator registers metadata on the class, indicating it can be instantiated
* by the container. While not strictly required for basic usage, it's recommended
* for explicit declaration and better IDE support.
*
* @returns Class decorator that marks the class as injectable
*
* @example
* ```typescript
* @injectable()
* class UserService {
* constructor(private db: Database, private logger: Logger) {}
*
* async getUser(id: string) {
* this.logger.log(`Fetching user ${id}`);
* return this.db.users.findById(id);
* }
* }
*
* container.register(Database);
* container.register(Logger);
* container.register(UserService);
* const service = container.resolve(UserService);
* ```
*
* @example
* ```typescript
* // Combining with explicit lifetime decorators
* @injectable()
* class RequestHandler {
* constructor(private service: UserService) {}
* }
* ```
*/
export function injectable(): ClassDecorator {
return function (target: Function) {
defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
return target as any;
};
}
/**
* Explicitly specify a token for constructor parameter injection.
*
* Use this decorator when TypeScript's automatic type detection isn't sufficient,
* such as when using interfaces, abstract classes, or string/symbol tokens.
* The decorator is applied to constructor parameters to override default resolution.
*
* @param token - The token to use for resolving this dependency
* @returns Parameter decorator that specifies the injection token
*
* @example
* ```typescript
* // Injecting with string tokens
* class UserService {
* constructor(
* @inject('IDatabase') private db: Database,
* @inject('ILogger') private logger: Logger
* ) {}
* }
*
* container.register('IDatabase', PostgresDatabase);
* container.register('ILogger', ConsoleLogger);
* container.register(UserService);
* ```
*
* @example
* ```typescript
* // Injecting with symbol tokens
* const DB_TOKEN = Symbol('database');
* const CACHE_TOKEN = Symbol('cache');
*
* class Repository {
* constructor(
* @inject(DB_TOKEN) private db: Database,
* @inject(CACHE_TOKEN) private cache: Cache
* ) {}
* }
* ```
*
* @example
* ```typescript
* // Abstract class injection
* abstract class Logger {
* abstract log(msg: string): void;
* }
*
* class ConsoleLogger extends Logger {
* log(msg: string) { console.log(msg); }
* }
*
* class UserService {
* constructor(@inject(Logger) private logger: Logger) {}
* }
*
* container.register(Logger, ConsoleLogger);
* container.register(UserService);
* ```
*/
export function inject(token: Token): ParameterDecorator {
return function (target: Object, _propertyKey: string | symbol | undefined, parameterIndex: number) {
const existingInjections = getMetadata(METADATA_KEYS.INJECT, target) || [];
existingInjections[parameterIndex] = token;
defineMetadata(METADATA_KEYS.INJECT, existingInjections, target);
};
}
/**
* Mark a class as a singleton service with a single shared instance.
*
* Singleton services are instantiated once and reused for all subsequent resolutions.
* This decorator automatically marks the class as injectable and sets its lifetime
* to Singleton. Ideal for stateful services, configuration, or expensive resources.
*
* @returns Class decorator that marks the class as singleton
*
* @example
* ```typescript
* @singleton()
* class DatabaseConnection {
* private connection?: Connection;
*
* async connect() {
* this.connection = await createConnection();
* }
*
* query(sql: string) {
* return this.connection!.execute(sql);
* }
* }
*
* container.register(DatabaseConnection);
* const db1 = container.resolve(DatabaseConnection);
* const db2 = container.resolve(DatabaseConnection);
* // db1 === db2 (same instance)
* ```
*
* @example
* ```typescript
* // Configuration singleton
* @singleton()
* class AppConfig {
* readonly apiUrl = process.env.API_URL || 'http://localhost';
* readonly port = parseInt(process.env.PORT || '3000');
* readonly env = process.env.NODE_ENV || 'development';
* }
* ```
*
* @example
* ```typescript
* // Shared cache
* @singleton()
* class CacheService {
* private cache = new Map<string, any>();
*
* set(key: string, value: any) { this.cache.set(key, value); }
* get(key: string) { return this.cache.get(key); }
* }
* ```
*/
export function singleton(): ClassDecorator {
return function (target: Function) {
defineMetadata(METADATA_KEYS.LIFETIME, Lifetime.Singleton, target);
defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
return target as any;
};
}
/**
* Mark a class as transient with a new instance created on every resolution.
*
* Transient services are never cached or reused. Each call to resolve creates
* a fresh instance. This decorator automatically marks the class as injectable
* and sets its lifetime to Transient. Use for stateless services or when
* isolation between uses is required.
*
* @returns Class decorator that marks the class as transient
*
* @example
* ```typescript
* @transient()
* class RequestLogger {
* private logs: string[] = [];
*
* log(message: string) {
* this.logs.push(`${new Date().toISOString()}: ${message}`);
* }
*
* getLogs() { return this.logs; }
* }
*
* container.register(RequestLogger);
* const logger1 = container.resolve(RequestLogger);
* const logger2 = container.resolve(RequestLogger);
* // logger1 !== logger2 (different instances)
* ```
*
* @example
* ```typescript
* // Command pattern with transient handlers
* @transient()
* class CreateUserCommand {
* constructor(private db: Database) {}
*
* async execute(userData: UserData) {
* return this.db.users.create(userData);
* }
* }
*
* // Each command execution gets a fresh handler
* const cmd1 = container.resolve(CreateUserCommand);
* await cmd1.execute(user1Data);
*
* const cmd2 = container.resolve(CreateUserCommand);
* await cmd2.execute(user2Data);
* ```
*
* @example
* ```typescript
* // Temporary buffer for data processing
* @transient()
* class DataBuffer {
* private buffer: any[] = [];
*
* add(item: any) { this.buffer.push(item); }
* flush() { return this.buffer.splice(0); }
* }
* ```
*/
export function transient(): ClassDecorator {
return function (target: Function) {
defineMetadata(METADATA_KEYS.LIFETIME, Lifetime.Transient, target);
defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
return target as any;
};
}
/**
* Mark a class as scoped with one instance per scope (child container).
*
* Scoped services are instantiated once per scope and reused within that scope,
* but isolated across different scopes. This decorator automatically marks the
* class as injectable and sets its lifetime to Scoped. Perfect for request-scoped
* services in web applications or transaction boundaries.
*
* @returns Class decorator that marks the class as scoped
*
* @example
* ```typescript
* @scoped()
* class RequestContext {
* constructor(
* @inject('requestId') public requestId: string,
* @inject('userId') public userId: string
* ) {}
*
* log(message: string) {
* console.log(`[${this.requestId}] [${this.userId}] ${message}`);
* }
* }
*
* // In web application
* app.use((req, res, next) => {
* req.scope = container.createScope();
* req.scope.registerValue('requestId', req.id);
* req.scope.registerValue('userId', req.user.id);
* next();
* });
*
* app.get('/api/data', (req, res) => {
* const ctx1 = req.scope.resolve(RequestContext);
* const ctx2 = req.scope.resolve(RequestContext);
* // ctx1 === ctx2 (same within scope)
* });
* ```
*
* @example
* ```typescript
* // Database transaction scope
* @scoped()
* class UnitOfWork {
* private transaction?: Transaction;
*
* async begin() {
* this.transaction = await db.beginTransaction();
* }
*
* async commit() {
* await this.transaction?.commit();
* }
*
* async rollback() {
* await this.transaction?.rollback();
* }
* }
*
* const scope = container.createScope();
* const uow = scope.resolve(UnitOfWork);
* await uow.begin();
* // ... perform operations within transaction
* await uow.commit();
* ```
*
* @example
* ```typescript
* // Request-scoped cache
* @scoped()
* class RequestCache {
* private cache = new Map<string, any>();
*
* get(key: string) { return this.cache.get(key); }
* set(key: string, value: any) { this.cache.set(key, value); }
* }
* ```
*/
export function scoped(): ClassDecorator {
return function (target: Function) {
defineMetadata(METADATA_KEYS.LIFETIME, Lifetime.Scoped, target);
defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
return target as any;
};
}