@martel/calyx 1.11.0 → 1.13.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/cache/cache.interceptor.ts +4 -2
  4. package/src/cache/decorators.ts +4 -0
  5. package/src/cache/index.ts +1 -0
  6. package/src/cli/index.ts +7 -1
  7. package/src/config/config.module.ts +16 -2
  8. package/src/config/config.service.ts +20 -6
  9. package/src/core/container.ts +559 -140
  10. package/src/core/index.ts +2 -0
  11. package/src/core/lazy-module-loader.ts +29 -0
  12. package/src/core/metadata.ts +6 -1
  13. package/src/core/testing-module.ts +123 -0
  14. package/src/cqrs/cqrs.ts +264 -0
  15. package/src/database/sequelize.module.ts +239 -0
  16. package/src/event-emitter/decorators.ts +2 -2
  17. package/src/event-emitter/event-emitter.ts +3 -0
  18. package/src/graphql/decorators.ts +16 -0
  19. package/src/graphql/graphql.module.ts +16 -0
  20. package/src/http/application.ts +261 -21
  21. package/src/http/decorators.ts +25 -1
  22. package/src/http/exceptions.ts +97 -0
  23. package/src/http/factory.ts +3 -0
  24. package/src/http/router.ts +27 -4
  25. package/src/index.ts +3 -0
  26. package/src/microservices/clients.module.ts +47 -0
  27. package/src/microservices/exceptions.ts +10 -0
  28. package/src/microservices/index.ts +2 -0
  29. package/src/microservices/microservice.ts +1 -1
  30. package/src/queue/queue.module.ts +73 -5
  31. package/src/schedule/decorators.ts +10 -6
  32. package/src/schedule/index.ts +1 -0
  33. package/src/schedule/schedule.module.ts +3 -2
  34. package/src/schedule/scheduler-registry.ts +50 -0
  35. package/src/security/index.ts +1 -0
  36. package/src/security/throttler.module.ts +108 -0
  37. package/src/terminus/terminus.ts +134 -0
  38. package/src/validation/compiler.ts +133 -10
  39. package/src/validation/decorators.ts +164 -2
  40. package/src/validation/http-pipes.ts +128 -0
  41. package/src/validation/index.ts +1 -0
  42. package/src/websockets/decorators.ts +12 -2
  43. package/src/websockets/exceptions.ts +10 -0
  44. package/src/websockets/index.ts +1 -0
  45. package/tests/circular-di.test.ts +151 -0
  46. package/tests/di.test.ts +10 -2
  47. package/tests/nestjs-parity.test.ts +527 -0
@@ -47,14 +47,14 @@ class ContainerModuleRef extends ModuleRef {
47
47
  const requestContext = contextId instanceof Map ? contextId : new Map<any, any>();
48
48
  const strict = options?.strict ?? true;
49
49
  if (strict) {
50
- return this.container.resolveTokenInModuleContext(this.moduleClass, token, requestContext);
50
+ return await this.container.resolveTokenInModuleContextAsync(this.moduleClass, token, requestContext);
51
51
  } else {
52
- return this.container.resolveTokenGlobally(token, requestContext);
52
+ return await this.container.resolveTokenGloballyAsync(token, requestContext);
53
53
  }
54
54
  }
55
55
 
56
56
  async create<T>(type: Type<T>): Promise<T> {
57
- return this.container.instantiateClass(type, this.moduleClass);
57
+ return await this.container.instantiateClassAsync(type, this.moduleClass);
58
58
  }
59
59
  }
60
60
 
@@ -65,6 +65,7 @@ export class CalyxContainer {
65
65
  private providerScopes = new Map<InjectionToken, Scope>();
66
66
  private controllerScopes = new Map<any, Scope>();
67
67
  private compiledControllerFactories = new Map<any, (requestContext: Map<any, any>) => any>();
68
+ private deferredProxies: { moduleClass: any; token: any; proxyTarget: { instance: any } }[] = [];
68
69
 
69
70
  getModuleRecord(moduleClass: any): ModuleRecord | undefined {
70
71
  return this.modules.get(resolveForwardRef(moduleClass));
@@ -124,29 +125,9 @@ export class CalyxContainer {
124
125
  this.addModule(actualModule);
125
126
  record.imports.add(actualModule);
126
127
 
127
- // If it is dynamic module, merge its extra providers/exports/controllers
128
+ // If it is dynamic module, merge its extra metadata recursively
128
129
  if (isDynamicModule(resolvedImp)) {
129
- const dynamicRecord = this.modules.get(actualModule)!;
130
- if (resolvedImp.providers) {
131
- for (const prov of resolvedImp.providers) {
132
- const token = this.getProviderToken(prov);
133
- dynamicRecord.providers.set(token, prov);
134
- }
135
- }
136
- if (resolvedImp.controllers) {
137
- for (const ctrl of resolvedImp.controllers) {
138
- dynamicRecord.controllers.add(ctrl);
139
- }
140
- }
141
- if (resolvedImp.exports) {
142
- for (const exp of resolvedImp.exports) {
143
- const resolvedExp = resolveForwardRef(exp);
144
- dynamicRecord.exports.add(resolvedExp);
145
- }
146
- }
147
- if (resolvedImp.global) {
148
- this.globalModules.add(actualModule);
149
- }
130
+ this.mergeDynamicModuleMetadata(resolvedImp);
150
131
  }
151
132
  }
152
133
 
@@ -171,6 +152,45 @@ export class CalyxContainer {
171
152
  }
172
153
  }
173
154
 
155
+ private mergeDynamicModuleMetadata(dynModule: DynamicModule) {
156
+ const actualModule = dynModule.module;
157
+ const record = this.modules.get(actualModule)!;
158
+
159
+ if (dynModule.imports) {
160
+ for (const imp of dynModule.imports) {
161
+ const resolvedImp = resolveForwardRef(imp);
162
+ const actualImp = isDynamicModule(resolvedImp) ? resolvedImp.module : resolvedImp;
163
+
164
+ this.addModule(actualImp);
165
+ record.imports.add(actualImp);
166
+
167
+ if (isDynamicModule(resolvedImp)) {
168
+ this.mergeDynamicModuleMetadata(resolvedImp);
169
+ }
170
+ }
171
+ }
172
+ if (dynModule.providers) {
173
+ for (const prov of dynModule.providers) {
174
+ const token = this.getProviderToken(prov);
175
+ record.providers.set(token, prov);
176
+ }
177
+ }
178
+ if (dynModule.controllers) {
179
+ for (const ctrl of dynModule.controllers) {
180
+ record.controllers.add(ctrl);
181
+ }
182
+ }
183
+ if (dynModule.exports) {
184
+ for (const exp of dynModule.exports) {
185
+ const resolvedExp = resolveForwardRef(exp);
186
+ record.exports.add(resolvedExp);
187
+ }
188
+ }
189
+ if (dynModule.global) {
190
+ this.globalModules.add(actualModule);
191
+ }
192
+ }
193
+
174
194
  private getProviderToken(provider: Provider): InjectionToken {
175
195
  if (typeof provider === 'function') {
176
196
  return provider;
@@ -178,19 +198,82 @@ export class CalyxContainer {
178
198
  return provider.provide;
179
199
  }
180
200
 
201
+ private resolveDeferredProxies() {
202
+ for (const dp of this.deferredProxies) {
203
+ if (!dp.proxyTarget.instance) {
204
+ const record = this.modules.get(dp.moduleClass);
205
+ if (record && record.instances.has(dp.token)) {
206
+ dp.proxyTarget.instance = record.instances.get(dp.token);
207
+ } else {
208
+ try {
209
+ dp.proxyTarget.instance = this.getGlobalOrAnyInstance(dp.token);
210
+ } catch {
211
+ // ignore
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
181
218
  resolveTokenInModuleContext<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): T {
219
+ const resolvedToken = resolveForwardRef(token);
182
220
  // 1. Check circular dependency
183
221
  const isResolving = this.resolvingStack.some(
184
- (item) => item.moduleClass === moduleClass && item.token === token
222
+ (item) => item.moduleClass === moduleClass && item.token === resolvedToken
185
223
  );
186
224
  if (isResolving) {
187
- const path = this.resolvingStack
188
- .map((item) => `${item.moduleClass.name ?? item.moduleClass}::${String(item.token.name ?? item.token)}`)
189
- .join(' -> ');
190
- throw new Error(`Circular dependency detected: ${path} -> ${moduleClass.name ?? moduleClass}::${String(token.name ?? token)}`);
225
+ const container = this;
226
+ const proxyTarget = { instance: null as any };
227
+ this.deferredProxies.push({
228
+ moduleClass,
229
+ token: resolvedToken,
230
+ proxyTarget,
231
+ });
232
+ return new Proxy(proxyTarget, {
233
+ get(target, prop) {
234
+ if (prop === '__isCalyxProxy') return true;
235
+ if (prop === '__proxyTarget') return target;
236
+ if (!target.instance) {
237
+ const record = container.modules.get(moduleClass);
238
+ if (record && record.instances.has(resolvedToken)) {
239
+ target.instance = record.instances.get(resolvedToken);
240
+ }
241
+ }
242
+ if (!target.instance) {
243
+ try {
244
+ target.instance = container.getGlobalOrAnyInstance(resolvedToken);
245
+ } catch {
246
+ // ignore
247
+ }
248
+ }
249
+ if (!target.instance) {
250
+ if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
251
+ return undefined;
252
+ }
253
+ throw new Error(`Calyx circular proxy DI error: Accessing property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
254
+ }
255
+ const value = Reflect.get(target.instance, prop);
256
+ if (typeof value === 'function') {
257
+ return value.bind(target.instance);
258
+ }
259
+ return value;
260
+ },
261
+ set(target, prop, value) {
262
+ if (!target.instance) {
263
+ const record = container.modules.get(moduleClass);
264
+ if (record && record.instances.has(resolvedToken)) {
265
+ target.instance = record.instances.get(resolvedToken);
266
+ }
267
+ }
268
+ if (!target.instance) {
269
+ throw new Error(`Calyx circular proxy DI error: Setting property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
270
+ }
271
+ return Reflect.set(target.instance, prop, value);
272
+ }
273
+ }) as any;
191
274
  }
192
275
 
193
- this.resolvingStack.push({ moduleClass, token });
276
+ this.resolvingStack.push({ moduleClass, token: resolvedToken });
194
277
 
195
278
  try {
196
279
  const record = this.modules.get(moduleClass);
@@ -199,14 +282,14 @@ export class CalyxContainer {
199
282
  }
200
283
 
201
284
  // Special resolution cases
202
- if (token === REQUEST) {
285
+ if (resolvedToken === REQUEST) {
203
286
  if (!requestContext || !requestContext.has(REQUEST)) {
204
287
  throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
205
288
  }
206
289
  return requestContext.get(REQUEST);
207
290
  }
208
291
 
209
- if (token === ModuleRef) {
292
+ if (resolvedToken === ModuleRef) {
210
293
  const instance = record.instances.get(ModuleRef);
211
294
  if (instance) return instance;
212
295
  const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
@@ -214,16 +297,17 @@ export class CalyxContainer {
214
297
  return moduleRefInstance as any;
215
298
  }
216
299
 
217
- if (token === moduleClass) {
300
+ if (resolvedToken === moduleClass) {
218
301
  const instance = record.instances.get(moduleClass);
219
302
  if (instance) return instance;
220
303
  const instantiatedModule = this.instantiateClass(moduleClass, moduleClass, requestContext);
221
304
  record.instances.set(moduleClass, instantiatedModule);
305
+ this.resolveDeferredProxies();
222
306
  return instantiatedModule;
223
307
  }
224
308
 
225
309
  // Find where the token is defined
226
- const resolution = this.findProviderDefinition(moduleClass, token);
310
+ const resolution = this.findProviderDefinition(moduleClass, resolvedToken);
227
311
  if (!resolution) {
228
312
  // Not found. Check if the token is optional
229
313
  const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
@@ -238,7 +322,7 @@ export class CalyxContainer {
238
322
  for (let i = 0; i < paramTypes.length; i++) {
239
323
  const pToken = injectTokens.get(i) ?? paramTypes[i];
240
324
  const resolvedPToken = resolveForwardRef(pToken);
241
- if (resolvedPToken === token && optionalParams.has(i)) {
325
+ if (resolvedPToken === resolvedToken && optionalParams.has(i)) {
242
326
  isOptional = true;
243
327
  break;
244
328
  }
@@ -248,23 +332,24 @@ export class CalyxContainer {
248
332
  }
249
333
  }
250
334
  }
251
- throw new Error(`calyx DI: Cannot resolve dependency "${String(token.name ?? token)}" in module ${moduleClass.name || moduleClass}`);
335
+ throw new Error(`calyx DI: Cannot resolve dependency "${String(resolvedToken.name ?? resolvedToken)}" in module ${moduleClass.name || moduleClass}`);
252
336
  }
253
337
 
254
338
  const { targetModuleClass, provider } = resolution;
255
339
  const targetRecord = this.modules.get(targetModuleClass)!;
256
340
 
257
- const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
341
+ const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
258
342
 
259
343
  if (scope === Scope.REQUEST) {
260
344
  if (!requestContext) {
261
- throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(token.name ?? token)}" without request context.`);
345
+ throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(resolvedToken.name ?? resolvedToken)}" without request context.`);
262
346
  }
263
- if (requestContext.has(token)) {
264
- return requestContext.get(token);
347
+ if (requestContext.has(resolvedToken)) {
348
+ return requestContext.get(resolvedToken);
265
349
  }
266
350
  const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
267
- requestContext.set(token, instance);
351
+ requestContext.set(resolvedToken, instance);
352
+ this.resolveDeferredProxies();
268
353
  return instance;
269
354
  }
270
355
 
@@ -273,12 +358,13 @@ export class CalyxContainer {
273
358
  }
274
359
 
275
360
  // Check if instance already exists in target module (Singleton)
276
- if (targetRecord.instances.has(token)) {
277
- return targetRecord.instances.get(token);
361
+ if (targetRecord.instances.has(resolvedToken)) {
362
+ return targetRecord.instances.get(resolvedToken);
278
363
  }
279
364
 
280
365
  const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
281
- targetRecord.instances.set(token, instance);
366
+ targetRecord.instances.set(resolvedToken, instance);
367
+ this.resolveDeferredProxies();
282
368
  return instance;
283
369
  } finally {
284
370
  this.resolvingStack.pop();
@@ -288,6 +374,8 @@ export class CalyxContainer {
288
374
  private createInstanceFromProvider(provider: Provider, targetModuleClass: any, requestContext?: Map<any, any>): any {
289
375
  if (typeof provider !== 'function' && 'useValue' in provider) {
290
376
  return provider.useValue;
377
+ } else if (typeof provider !== 'function' && 'useExisting' in provider) {
378
+ return this.resolveTokenInModuleContext(targetModuleClass, resolveForwardRef(provider.useExisting), requestContext);
291
379
  } else if (typeof provider !== 'function' && 'useClass' in provider) {
292
380
  return this.instantiateClass(provider.useClass, targetModuleClass, requestContext);
293
381
  } else if (typeof provider !== 'function' && 'useFactory' in provider) {
@@ -400,7 +488,7 @@ export class CalyxContainer {
400
488
  const propertyInjects: Map<string | symbol, InjectionToken> =
401
489
  Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
402
490
  for (const [propertyKey, token] of propertyInjects.entries()) {
403
- instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, token, requestContext);
491
+ instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, resolveForwardRef(token), requestContext);
404
492
  }
405
493
 
406
494
  return instance;
@@ -415,13 +503,31 @@ export class CalyxContainer {
415
503
  throw new Error(`calyx DI: Instance of "${String(token.name ?? token)}" not found in any module context`);
416
504
  }
417
505
 
418
- bootstrap(rootModule: any) {
506
+ bootstrap(rootModule: any): void | Promise<void> {
419
507
  this.addModule(rootModule);
420
-
421
- // Compute scopes of all providers and controllers (including bubble-up)
422
508
  this.resolveProviderAndControllerScopes();
423
509
 
424
- // Instantiate all singleton providers
510
+ let hasAsync = false;
511
+ for (const record of this.modules.values()) {
512
+ for (const provider of record.providers.values()) {
513
+ if (typeof provider !== 'function' && 'useFactory' in provider) {
514
+ if (provider.useFactory.constructor.name === 'AsyncFunction') {
515
+ hasAsync = true;
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ if (hasAsync) break;
521
+ }
522
+
523
+ if (hasAsync) {
524
+ return this.bootstrapAsync(rootModule);
525
+ }
526
+
527
+ this.bootstrapSync(rootModule);
528
+ }
529
+
530
+ private bootstrapSync(rootModule: any) {
425
531
  for (const [moduleClass, record] of this.modules.entries()) {
426
532
  for (const token of record.providers.keys()) {
427
533
  const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
@@ -431,7 +537,6 @@ export class CalyxContainer {
431
537
  }
432
538
  }
433
539
 
434
- // Instantiate all singleton controllers
435
540
  for (const [moduleClass, record] of this.modules.entries()) {
436
541
  for (const controllerClass of record.controllers) {
437
542
  const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
@@ -441,14 +546,46 @@ export class CalyxContainer {
441
546
  }
442
547
  }
443
548
 
444
- // Instantiate module classes themselves
445
549
  for (const [moduleClass, record] of this.modules.entries()) {
446
550
  if (record.instances.get(moduleClass) === null) {
447
551
  const instance = this.instantiateClass(moduleClass, moduleClass);
448
552
  record.instances.set(moduleClass, instance);
553
+ this.resolveDeferredProxies();
449
554
  }
450
555
  }
451
556
 
557
+ this.resolveDeferredProxies();
558
+ this.compileControllerFactories();
559
+ }
560
+
561
+ private async bootstrapAsync(rootModule: any) {
562
+ for (const [moduleClass, record] of this.modules.entries()) {
563
+ for (const token of record.providers.keys()) {
564
+ const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
565
+ if (scope === Scope.DEFAULT) {
566
+ await this.resolveTokenInModuleContextAsync(moduleClass, token);
567
+ }
568
+ }
569
+ }
570
+
571
+ for (const [moduleClass, record] of this.modules.entries()) {
572
+ for (const controllerClass of record.controllers) {
573
+ const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
574
+ if (scope === Scope.DEFAULT) {
575
+ await this.resolveControllerAsync(moduleClass, controllerClass);
576
+ }
577
+ }
578
+ }
579
+
580
+ for (const [moduleClass, record] of this.modules.entries()) {
581
+ if (record.instances.get(moduleClass) === null) {
582
+ const instance = await this.instantiateClassAsync(moduleClass, moduleClass);
583
+ record.instances.set(moduleClass, instance);
584
+ this.resolveDeferredProxies();
585
+ }
586
+ }
587
+
588
+ this.resolveDeferredProxies();
452
589
  this.compileControllerFactories();
453
590
  }
454
591
 
@@ -459,6 +596,18 @@ export class CalyxContainer {
459
596
  }
460
597
  const instance = this.instantiateClass(controllerClass, moduleClass);
461
598
  record.instances.set(controllerClass, instance);
599
+ this.resolveDeferredProxies();
600
+ return instance;
601
+ }
602
+
603
+ private async resolveControllerAsync(moduleClass: any, controllerClass: any): Promise<any> {
604
+ const record = this.modules.get(moduleClass)!;
605
+ if (record.instances.has(controllerClass)) {
606
+ return record.instances.get(controllerClass);
607
+ }
608
+ const instance = await this.instantiateClassAsync(controllerClass, moduleClass);
609
+ record.instances.set(controllerClass, instance);
610
+ this.resolveDeferredProxies();
462
611
  return instance;
463
612
  }
464
613
 
@@ -475,6 +624,229 @@ export class CalyxContainer {
475
624
  return this.getGlobalOrAnyInstance(token);
476
625
  }
477
626
 
627
+ async resolveTokenGloballyAsync<T>(token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
628
+ const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
629
+ if (scope === Scope.REQUEST && requestContext?.has(token)) {
630
+ return requestContext.get(token);
631
+ }
632
+ for (const [moduleClass, record] of this.modules.entries()) {
633
+ if (record.providers.has(token)) {
634
+ return await this.resolveTokenInModuleContextAsync(moduleClass, token, requestContext);
635
+ }
636
+ }
637
+ return this.getGlobalOrAnyInstance(token);
638
+ }
639
+
640
+ async resolveTokenInModuleContextAsync<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
641
+ const resolvedToken = resolveForwardRef(token);
642
+ const isResolving = this.resolvingStack.some(
643
+ (item) => item.moduleClass === moduleClass && item.token === resolvedToken
644
+ );
645
+ if (isResolving) {
646
+ const container = this;
647
+ const proxyTarget = { instance: null as any };
648
+ this.deferredProxies.push({
649
+ moduleClass,
650
+ token: resolvedToken,
651
+ proxyTarget,
652
+ });
653
+ return new Proxy(proxyTarget, {
654
+ get(target, prop) {
655
+ if (prop === '__isCalyxProxy') return true;
656
+ if (prop === '__proxyTarget') return target;
657
+ if (!target.instance) {
658
+ const record = container.modules.get(moduleClass);
659
+ if (record && record.instances.has(resolvedToken)) {
660
+ target.instance = record.instances.get(resolvedToken);
661
+ }
662
+ }
663
+ if (!target.instance) {
664
+ try {
665
+ target.instance = container.getGlobalOrAnyInstance(resolvedToken);
666
+ } catch {
667
+ // ignore
668
+ }
669
+ }
670
+ if (!target.instance) {
671
+ if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
672
+ return undefined;
673
+ }
674
+ throw new Error(`Calyx circular proxy DI error: Accessing property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
675
+ }
676
+ const value = Reflect.get(target.instance, prop);
677
+ if (typeof value === 'function') {
678
+ return value.bind(target.instance);
679
+ }
680
+ return value;
681
+ },
682
+ set(target, prop, value) {
683
+ if (!target.instance) {
684
+ const record = container.modules.get(moduleClass);
685
+ if (record && record.instances.has(resolvedToken)) {
686
+ target.instance = record.instances.get(resolvedToken);
687
+ }
688
+ }
689
+ if (!target.instance) {
690
+ throw new Error(`Calyx circular proxy DI error: Setting property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
691
+ }
692
+ return Reflect.set(target.instance, prop, value);
693
+ }
694
+ }) as any;
695
+ }
696
+
697
+ this.resolvingStack.push({ moduleClass, token: resolvedToken });
698
+
699
+ try {
700
+ const record = this.modules.get(moduleClass);
701
+ if (!record) {
702
+ throw new Error(`Module ${moduleClass.name || moduleClass} is not registered in the container`);
703
+ }
704
+
705
+ if (resolvedToken === REQUEST) {
706
+ if (!requestContext || !requestContext.has(REQUEST)) {
707
+ throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
708
+ }
709
+ return requestContext.get(REQUEST);
710
+ }
711
+
712
+ if (resolvedToken === ModuleRef) {
713
+ const instance = record.instances.get(ModuleRef);
714
+ if (instance) return instance;
715
+ const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
716
+ record.instances.set(ModuleRef, moduleRefInstance);
717
+ return moduleRefInstance as any;
718
+ }
719
+
720
+ if (resolvedToken === moduleClass) {
721
+ const instance = record.instances.get(moduleClass);
722
+ if (instance) return instance;
723
+ const instantiatedModule = await this.instantiateClassAsync(moduleClass, moduleClass, requestContext);
724
+ record.instances.set(moduleClass, instantiatedModule);
725
+ this.resolveDeferredProxies();
726
+ return instantiatedModule;
727
+ }
728
+
729
+ const resolution = this.findProviderDefinition(moduleClass, resolvedToken);
730
+ if (!resolution) {
731
+ const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
732
+ if (currentResolving) {
733
+ const parentClass = currentResolving.token;
734
+ if (typeof parentClass === 'function') {
735
+ const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, parentClass) || new Set();
736
+ const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, parentClass) || new Map();
737
+ const paramTypes = Reflect.getMetadata('design:paramtypes', parentClass) || [];
738
+
739
+ let isOptional = false;
740
+ for (let i = 0; i < paramTypes.length; i++) {
741
+ const pToken = injectTokens.get(i) ?? paramTypes[i];
742
+ const resolvedPToken = resolveForwardRef(pToken);
743
+ if (resolvedPToken === resolvedToken && optionalParams.has(i)) {
744
+ isOptional = true;
745
+ break;
746
+ }
747
+ }
748
+ if (isOptional) {
749
+ return undefined as any;
750
+ }
751
+ }
752
+ }
753
+ throw new Error(`calyx DI: Cannot resolve dependency "${String(resolvedToken.name ?? resolvedToken)}" in module ${moduleClass.name || moduleClass}`);
754
+ }
755
+
756
+ const { targetModuleClass, provider } = resolution;
757
+ const targetRecord = this.modules.get(targetModuleClass)!;
758
+
759
+ const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
760
+
761
+ if (scope === Scope.REQUEST) {
762
+ if (!requestContext) {
763
+ throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(resolvedToken.name ?? resolvedToken)}" without request context.`);
764
+ }
765
+ if (requestContext.has(resolvedToken)) {
766
+ return requestContext.get(resolvedToken);
767
+ }
768
+ const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
769
+ requestContext.set(resolvedToken, instance);
770
+ this.resolveDeferredProxies();
771
+ return instance;
772
+ }
773
+
774
+ if (scope === Scope.TRANSIENT) {
775
+ return await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
776
+ }
777
+
778
+ if (targetRecord.instances.has(resolvedToken)) {
779
+ return targetRecord.instances.get(resolvedToken);
780
+ }
781
+
782
+ const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
783
+ targetRecord.instances.set(resolvedToken, instance);
784
+ this.resolveDeferredProxies();
785
+ return instance;
786
+ } finally {
787
+ this.resolvingStack.pop();
788
+ }
789
+ }
790
+
791
+ private async createInstanceFromProviderAsync(provider: Provider, targetModuleClass: any, requestContext?: Map<any, any>): Promise<any> {
792
+ if (typeof provider !== 'function' && 'useValue' in provider) {
793
+ return provider.useValue;
794
+ } else if (typeof provider !== 'function' && 'useExisting' in provider) {
795
+ return await this.resolveTokenInModuleContextAsync(targetModuleClass, resolveForwardRef(provider.useExisting), requestContext);
796
+ } else if (typeof provider !== 'function' && 'useClass' in provider) {
797
+ return await this.instantiateClassAsync(provider.useClass, targetModuleClass, requestContext);
798
+ } else if (typeof provider !== 'function' && 'useFactory' in provider) {
799
+ const injectTokens = provider.inject || [];
800
+ const args = await Promise.all(
801
+ injectTokens.map((t) => this.resolveTokenInModuleContextAsync(targetModuleClass, resolveForwardRef(t), requestContext))
802
+ );
803
+ const res = provider.useFactory(...args);
804
+ return res instanceof Promise ? await res : res;
805
+ } else {
806
+ return await this.instantiateClassAsync(provider as Type<any>, targetModuleClass, requestContext);
807
+ }
808
+ }
809
+
810
+ public async instantiateClassAsync(Class: Type<any>, moduleClass: any, requestContext?: Map<any, any>): Promise<any> {
811
+ if (
812
+ !Reflect.hasMetadata(METADATA_KEYS.INJECTABLE, Class) &&
813
+ !Reflect.hasMetadata(METADATA_KEYS.CONTROLLER, Class) &&
814
+ !Reflect.hasMetadata(METADATA_KEYS.MODULE, Class)
815
+ ) {
816
+ throw new Error(`Class ${Class.name} is missing @Injectable(), @Controller() or @Module() decorator`);
817
+ }
818
+
819
+ const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', Class) || [];
820
+ const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, Class) || new Map();
821
+ const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, Class) || new Set();
822
+
823
+ const args = await Promise.all(
824
+ paramTypes.map(async (paramType, i) => {
825
+ const token = injectTokens.get(i) ?? paramType;
826
+ const resolvedToken = resolveForwardRef(token);
827
+
828
+ try {
829
+ return await this.resolveTokenInModuleContextAsync(moduleClass, resolvedToken, requestContext);
830
+ } catch (err) {
831
+ if (optionalParams.has(i)) {
832
+ return undefined;
833
+ }
834
+ throw err;
835
+ }
836
+ })
837
+ );
838
+
839
+ const instance = new Class(...args);
840
+
841
+ const propertyInjects: Map<string | symbol, InjectionToken> =
842
+ Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
843
+ for (const [propertyKey, token] of propertyInjects.entries()) {
844
+ instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, resolveForwardRef(token), requestContext);
845
+ }
846
+
847
+ return instance;
848
+ }
849
+
478
850
  resolveControllerInRequestContext(moduleClass: any, controllerClass: any, requestContext: Map<any, any>): any {
479
851
  const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
480
852
  if (scope === Scope.REQUEST) {
@@ -634,123 +1006,170 @@ export class CalyxContainer {
634
1006
  return idx;
635
1007
  };
636
1008
 
1009
+ const getLazyRequestProxy = (token: any, requestContext: Map<any, any>) => {
1010
+ const proxyTarget = { instance: null as any };
1011
+ return new Proxy(proxyTarget, {
1012
+ get(target, prop) {
1013
+ if (prop === '__isCalyxProxy') return true;
1014
+ if (prop === '__proxyTarget') return target;
1015
+ if (!target.instance) {
1016
+ target.instance = requestContext.get(token);
1017
+ }
1018
+ if (!target.instance) {
1019
+ if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
1020
+ return undefined;
1021
+ }
1022
+ throw new Error(`Calyx circular request proxy error: Accessing property "${String(prop)}" before it is instantiated.`);
1023
+ }
1024
+ const val = Reflect.get(target.instance, prop);
1025
+ if (typeof val === 'function') {
1026
+ return val.bind(target.instance);
1027
+ }
1028
+ return val;
1029
+ },
1030
+ set(target, prop, value) {
1031
+ if (!target.instance) {
1032
+ target.instance = requestContext.get(token);
1033
+ }
1034
+ if (!target.instance) {
1035
+ throw new Error(`Calyx circular request proxy error: Setting property "${String(prop)}" before it is instantiated.`);
1036
+ }
1037
+ return Reflect.set(target.instance, prop, value);
1038
+ }
1039
+ });
1040
+ };
1041
+
1042
+ const lazyProxyHelperIdx = getClosureIdx(getLazyRequestProxy);
1043
+
637
1044
  const instantiations: string[] = [];
638
1045
  const compiledTokens = new Set<string>();
1046
+ const compilingTokens = new Set<string>();
639
1047
 
640
1048
  const compileToken = (token: any, currentModuleClass: any): string => {
641
- if (token === REQUEST) {
1049
+ const resolvedToken = resolveForwardRef(token);
1050
+ if (resolvedToken === REQUEST) {
642
1051
  return `requestContext.get(c[${getClosureIdx(REQUEST)}])`;
643
1052
  }
644
- if (token === ModuleRef) {
1053
+ if (resolvedToken === ModuleRef) {
645
1054
  const record = this.modules.get(currentModuleClass)!;
646
1055
  return `c[${getClosureIdx(record.instances.get(ModuleRef))}]`;
647
1056
  }
648
1057
 
649
- const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
1058
+ const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
650
1059
  if (scope === Scope.DEFAULT) {
651
- const instance = this.resolveTokenInModuleContext(currentModuleClass, token);
1060
+ const instance = this.resolveTokenInModuleContext(currentModuleClass, resolvedToken);
652
1061
  return `c[${getClosureIdx(instance)}]`;
653
1062
  }
654
1063
 
655
- const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(token.name || token)}`;
1064
+ const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(resolvedToken.name || resolvedToken)}`;
656
1065
  if (compiledTokens.has(tokenKey)) {
657
- return `requestContext.get(c[${getClosureIdx(token)}])`;
1066
+ return `requestContext.get(c[${getClosureIdx(resolvedToken)}])`;
658
1067
  }
659
-
660
- const resolution = this.findProviderDefinition(currentModuleClass, token);
661
- if (!resolution) {
662
- const optional = this.isTokenOptional(token, currentModuleClass);
663
- return optional ? `undefined` : `(() => { throw new Error('calyx JIT DI: Cannot resolve dependency ' + String(c[${getClosureIdx(token)}])); })()`;
1068
+ if (compilingTokens.has(tokenKey)) {
1069
+ return `c[${lazyProxyHelperIdx}](c[${getClosureIdx(resolvedToken)}], requestContext)`;
664
1070
  }
665
1071
 
666
- const { targetModuleClass, provider } = resolution;
667
-
668
- let valExpr = '';
669
- if (typeof provider !== 'function' && 'useValue' in provider) {
670
- valExpr = `c[${getClosureIdx(provider.useValue)}]`;
671
- } else {
672
- const ClassToInstantiate = typeof provider === 'function'
673
- ? provider
674
- : ('useClass' in provider ? provider.useClass : null);
675
-
676
- if (ClassToInstantiate) {
677
- const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', ClassToInstantiate) || [];
678
- const injectTokens: Map<number, any> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, ClassToInstantiate) || new Map();
679
- const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, ClassToInstantiate) || new Set();
680
-
681
- const argsExprs = paramTypes.map((paramType, i) => {
682
- const depToken = injectTokens.get(i) ?? paramType;
683
- const resolvedDepToken = resolveForwardRef(depToken);
684
- try {
685
- return compileToken(resolvedDepToken, targetModuleClass);
686
- } catch (err) {
687
- if (optionalParams.has(i)) {
688
- return `undefined`;
1072
+ compilingTokens.add(tokenKey);
1073
+ try {
1074
+ const resolution = this.findProviderDefinition(currentModuleClass, resolvedToken);
1075
+ if (!resolution) {
1076
+ const optional = this.isTokenOptional(resolvedToken, currentModuleClass);
1077
+ return optional ? `undefined` : `(() => { throw new Error('calyx JIT DI: Cannot resolve dependency ' + String(c[${getClosureIdx(resolvedToken)}])); })()`;
1078
+ }
1079
+
1080
+ const { targetModuleClass, provider } = resolution;
1081
+
1082
+ let valExpr = '';
1083
+ if (typeof provider !== 'function' && 'useValue' in provider) {
1084
+ valExpr = `c[${getClosureIdx(provider.useValue)}]`;
1085
+ } else if (typeof provider !== 'function' && 'useExisting' in provider) {
1086
+ return compileToken(resolveForwardRef(provider.useExisting), targetModuleClass);
1087
+ } else {
1088
+ const ClassToInstantiate = typeof provider === 'function'
1089
+ ? provider
1090
+ : ('useClass' in provider ? provider.useClass : null);
1091
+
1092
+ if (ClassToInstantiate) {
1093
+ const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', ClassToInstantiate) || [];
1094
+ const injectTokens: Map<number, any> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, ClassToInstantiate) || new Map();
1095
+ const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, ClassToInstantiate) || new Set();
1096
+
1097
+ const argsExprs = paramTypes.map((paramType, i) => {
1098
+ const depToken = injectTokens.get(i) ?? paramType;
1099
+ const resolvedDepToken = resolveForwardRef(depToken);
1100
+ try {
1101
+ return compileToken(resolvedDepToken, targetModuleClass);
1102
+ } catch (err) {
1103
+ if (optionalParams.has(i)) {
1104
+ return `undefined`;
1105
+ }
1106
+ throw err;
689
1107
  }
690
- throw err;
1108
+ });
1109
+
1110
+ const propertyInjects: Map<string | symbol, any> =
1111
+ Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, ClassToInstantiate) || new Map();
1112
+
1113
+ const propAssignments: string[] = [];
1114
+ for (const [propKey, propToken] of propertyInjects.entries()) {
1115
+ const resolvedPropToken = resolveForwardRef(propToken);
1116
+ const propValExpr = compileToken(resolvedPropToken, targetModuleClass);
1117
+ propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
691
1118
  }
692
- });
693
-
694
- const propertyInjects: Map<string | symbol, any> =
695
- Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, ClassToInstantiate) || new Map();
696
-
697
- const propAssignments: string[] = [];
698
- for (const [propKey, propToken] of propertyInjects.entries()) {
699
- const resolvedPropToken = resolveForwardRef(propToken);
700
- const propValExpr = compileToken(resolvedPropToken, targetModuleClass);
701
- propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
702
- }
703
1119
 
704
- const classIdx = getClosureIdx(ClassToInstantiate);
705
- const instantiateExpr = `new c[${classIdx}](${argsExprs.join(', ')})`;
706
-
707
- if (scope === Scope.REQUEST) {
708
- compiledTokens.add(tokenKey);
709
- const tokenIdx = getClosureIdx(token);
710
- instantiations.push(`
711
- if (!requestContext.has(c[${tokenIdx}])) {
712
- const inst = ${instantiateExpr};
713
- ${propAssignments.join('\n')}
714
- requestContext.set(c[${tokenIdx}], inst);
1120
+ const classIdx = getClosureIdx(ClassToInstantiate);
1121
+ const instantiateExpr = `new c[${classIdx}](${argsExprs.join(', ')})`;
1122
+
1123
+ if (scope === Scope.REQUEST) {
1124
+ compiledTokens.add(tokenKey);
1125
+ const tokenIdx = getClosureIdx(resolvedToken);
1126
+ instantiations.push(`
1127
+ if (!requestContext.has(c[${tokenIdx}])) {
1128
+ const inst = ${instantiateExpr};
1129
+ ${propAssignments.join('\n')}
1130
+ requestContext.set(c[${tokenIdx}], inst);
1131
+ }
1132
+ `);
1133
+ valExpr = `requestContext.get(c[${tokenIdx}])`;
1134
+ } else {
1135
+ if (propAssignments.length > 0) {
1136
+ const tempVar = `transient_${compiledTokens.size}`;
1137
+ compiledTokens.add(tempVar);
1138
+ instantiations.push(`
1139
+ const ${tempVar} = ${instantiateExpr};
1140
+ ${propAssignments.map(line => line.replace('inst.', `${tempVar}.`)).join('\n')}
1141
+ `);
1142
+ valExpr = tempVar;
1143
+ } else {
1144
+ valExpr = instantiateExpr;
715
1145
  }
716
- `);
717
- valExpr = `requestContext.get(c[${tokenIdx}])`;
718
- } else {
719
- if (propAssignments.length > 0) {
720
- const tempVar = `transient_${compiledTokens.size}`;
721
- compiledTokens.add(tempVar);
1146
+ }
1147
+ } else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
1148
+ const injectTokens = provider.inject || [];
1149
+ const argsExprs = injectTokens.map((t) => compileToken(resolveForwardRef(t), targetModuleClass));
1150
+
1151
+ const factoryIdx = getClosureIdx(provider.useFactory);
1152
+ const factoryCallExpr = `c[${factoryIdx}](${argsExprs.join(', ')})`;
1153
+
1154
+ if (scope === Scope.REQUEST) {
1155
+ compiledTokens.add(tokenKey);
1156
+ const tokenIdx = getClosureIdx(resolvedToken);
722
1157
  instantiations.push(`
723
- const ${tempVar} = ${instantiateExpr};
724
- ${propAssignments.map(line => line.replace('inst.', `${tempVar}.`)).join('\n')}
1158
+ if (!requestContext.has(c[${tokenIdx}])) {
1159
+ requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
1160
+ }
725
1161
  `);
726
- valExpr = tempVar;
1162
+ valExpr = `requestContext.get(c[${tokenIdx}])`;
727
1163
  } else {
728
- valExpr = instantiateExpr;
1164
+ valExpr = factoryCallExpr;
729
1165
  }
730
1166
  }
731
- } else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
732
- const injectTokens = provider.inject || [];
733
- const argsExprs = injectTokens.map((t) => compileToken(resolveForwardRef(t), targetModuleClass));
734
-
735
- const factoryIdx = getClosureIdx(provider.useFactory);
736
- const factoryCallExpr = `c[${factoryIdx}](${argsExprs.join(', ')})`;
737
-
738
- if (scope === Scope.REQUEST) {
739
- compiledTokens.add(tokenKey);
740
- const tokenIdx = getClosureIdx(token);
741
- instantiations.push(`
742
- if (!requestContext.has(c[${tokenIdx}])) {
743
- requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
744
- }
745
- `);
746
- valExpr = `requestContext.get(c[${tokenIdx}])`;
747
- } else {
748
- valExpr = factoryCallExpr;
749
- }
750
1167
  }
751
- }
752
1168
 
753
- return valExpr;
1169
+ return valExpr;
1170
+ } finally {
1171
+ compilingTokens.delete(tokenKey);
1172
+ }
754
1173
  };
755
1174
 
756
1175
  const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', controllerClass) || [];