@martel/calyx 1.12.0 → 1.13.1

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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +5 -3
  3. package/benchmarks/graphql-benchmark.ts +4 -1
  4. package/benchmarks/serialization-benchmark.ts +46 -6
  5. package/benchmarks/techniques-benchmark.ts +12 -0
  6. package/benchmarks/validation-benchmark.ts +8 -1
  7. package/docs/controllers.md +3 -3
  8. package/docs/dependency-injection.md +3 -3
  9. package/docs/lifecycle.md +3 -3
  10. package/package.json +1 -1
  11. package/src/cli/index.ts +7 -1
  12. package/src/config/config.module.ts +16 -2
  13. package/src/config/config.service.ts +20 -6
  14. package/src/cookies/cookies.ts +45 -8
  15. package/src/core/container.ts +340 -154
  16. package/src/core/testing-module.ts +4 -0
  17. package/src/cqrs/cqrs.ts +93 -4
  18. package/src/database/sequelize.module.ts +239 -0
  19. package/src/event-emitter/decorators.ts +2 -2
  20. package/src/event-emitter/event-emitter.ts +3 -0
  21. package/src/graphql/graphql.module.ts +2 -4
  22. package/src/http/application.ts +140 -10
  23. package/src/http/decorators.ts +21 -1
  24. package/src/http/exceptions.ts +97 -0
  25. package/src/http/factory.ts +3 -0
  26. package/src/http/router.ts +27 -4
  27. package/src/index.ts +1 -0
  28. package/src/microservices/exceptions.ts +10 -0
  29. package/src/microservices/index.ts +1 -0
  30. package/src/queue/queue.module.ts +73 -5
  31. package/src/terminus/terminus.ts +75 -2
  32. package/src/validation/compiler.ts +137 -17
  33. package/src/validation/decorators.ts +164 -2
  34. package/src/websockets/exceptions.ts +10 -0
  35. package/src/websockets/index.ts +1 -0
  36. package/tests/circular-di.test.ts +151 -0
  37. package/tests/di.test.ts +10 -2
  38. package/tests/nestjs-parity.test.ts +255 -0
@@ -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();
@@ -402,7 +488,7 @@ export class CalyxContainer {
402
488
  const propertyInjects: Map<string | symbol, InjectionToken> =
403
489
  Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
404
490
  for (const [propertyKey, token] of propertyInjects.entries()) {
405
- instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, token, requestContext);
491
+ instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, resolveForwardRef(token), requestContext);
406
492
  }
407
493
 
408
494
  return instance;
@@ -464,9 +550,11 @@ export class CalyxContainer {
464
550
  if (record.instances.get(moduleClass) === null) {
465
551
  const instance = this.instantiateClass(moduleClass, moduleClass);
466
552
  record.instances.set(moduleClass, instance);
553
+ this.resolveDeferredProxies();
467
554
  }
468
555
  }
469
556
 
557
+ this.resolveDeferredProxies();
470
558
  this.compileControllerFactories();
471
559
  }
472
560
 
@@ -493,9 +581,11 @@ export class CalyxContainer {
493
581
  if (record.instances.get(moduleClass) === null) {
494
582
  const instance = await this.instantiateClassAsync(moduleClass, moduleClass);
495
583
  record.instances.set(moduleClass, instance);
584
+ this.resolveDeferredProxies();
496
585
  }
497
586
  }
498
587
 
588
+ this.resolveDeferredProxies();
499
589
  this.compileControllerFactories();
500
590
  }
501
591
 
@@ -506,6 +596,7 @@ export class CalyxContainer {
506
596
  }
507
597
  const instance = this.instantiateClass(controllerClass, moduleClass);
508
598
  record.instances.set(controllerClass, instance);
599
+ this.resolveDeferredProxies();
509
600
  return instance;
510
601
  }
511
602
 
@@ -516,6 +607,7 @@ export class CalyxContainer {
516
607
  }
517
608
  const instance = await this.instantiateClassAsync(controllerClass, moduleClass);
518
609
  record.instances.set(controllerClass, instance);
610
+ this.resolveDeferredProxies();
519
611
  return instance;
520
612
  }
521
613
 
@@ -546,17 +638,63 @@ export class CalyxContainer {
546
638
  }
547
639
 
548
640
  async resolveTokenInModuleContextAsync<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
641
+ const resolvedToken = resolveForwardRef(token);
549
642
  const isResolving = this.resolvingStack.some(
550
- (item) => item.moduleClass === moduleClass && item.token === token
643
+ (item) => item.moduleClass === moduleClass && item.token === resolvedToken
551
644
  );
552
645
  if (isResolving) {
553
- const path = this.resolvingStack
554
- .map((item) => `${item.moduleClass.name ?? item.moduleClass}::${String(item.token.name ?? item.token)}`)
555
- .join(' -> ');
556
- throw new Error(`Circular dependency detected: ${path} -> ${moduleClass.name ?? moduleClass}::${String(token.name ?? token)}`);
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;
557
695
  }
558
696
 
559
- this.resolvingStack.push({ moduleClass, token });
697
+ this.resolvingStack.push({ moduleClass, token: resolvedToken });
560
698
 
561
699
  try {
562
700
  const record = this.modules.get(moduleClass);
@@ -564,14 +702,14 @@ export class CalyxContainer {
564
702
  throw new Error(`Module ${moduleClass.name || moduleClass} is not registered in the container`);
565
703
  }
566
704
 
567
- if (token === REQUEST) {
705
+ if (resolvedToken === REQUEST) {
568
706
  if (!requestContext || !requestContext.has(REQUEST)) {
569
707
  throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
570
708
  }
571
709
  return requestContext.get(REQUEST);
572
710
  }
573
711
 
574
- if (token === ModuleRef) {
712
+ if (resolvedToken === ModuleRef) {
575
713
  const instance = record.instances.get(ModuleRef);
576
714
  if (instance) return instance;
577
715
  const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
@@ -579,15 +717,16 @@ export class CalyxContainer {
579
717
  return moduleRefInstance as any;
580
718
  }
581
719
 
582
- if (token === moduleClass) {
720
+ if (resolvedToken === moduleClass) {
583
721
  const instance = record.instances.get(moduleClass);
584
722
  if (instance) return instance;
585
723
  const instantiatedModule = await this.instantiateClassAsync(moduleClass, moduleClass, requestContext);
586
724
  record.instances.set(moduleClass, instantiatedModule);
725
+ this.resolveDeferredProxies();
587
726
  return instantiatedModule;
588
727
  }
589
728
 
590
- const resolution = this.findProviderDefinition(moduleClass, token);
729
+ const resolution = this.findProviderDefinition(moduleClass, resolvedToken);
591
730
  if (!resolution) {
592
731
  const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
593
732
  if (currentResolving) {
@@ -601,7 +740,7 @@ export class CalyxContainer {
601
740
  for (let i = 0; i < paramTypes.length; i++) {
602
741
  const pToken = injectTokens.get(i) ?? paramTypes[i];
603
742
  const resolvedPToken = resolveForwardRef(pToken);
604
- if (resolvedPToken === token && optionalParams.has(i)) {
743
+ if (resolvedPToken === resolvedToken && optionalParams.has(i)) {
605
744
  isOptional = true;
606
745
  break;
607
746
  }
@@ -611,23 +750,24 @@ export class CalyxContainer {
611
750
  }
612
751
  }
613
752
  }
614
- throw new Error(`calyx DI: Cannot resolve dependency "${String(token.name ?? token)}" in module ${moduleClass.name || moduleClass}`);
753
+ throw new Error(`calyx DI: Cannot resolve dependency "${String(resolvedToken.name ?? resolvedToken)}" in module ${moduleClass.name || moduleClass}`);
615
754
  }
616
755
 
617
756
  const { targetModuleClass, provider } = resolution;
618
757
  const targetRecord = this.modules.get(targetModuleClass)!;
619
758
 
620
- const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
759
+ const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
621
760
 
622
761
  if (scope === Scope.REQUEST) {
623
762
  if (!requestContext) {
624
- throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(token.name ?? token)}" without request context.`);
763
+ throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(resolvedToken.name ?? resolvedToken)}" without request context.`);
625
764
  }
626
- if (requestContext.has(token)) {
627
- return requestContext.get(token);
765
+ if (requestContext.has(resolvedToken)) {
766
+ return requestContext.get(resolvedToken);
628
767
  }
629
768
  const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
630
- requestContext.set(token, instance);
769
+ requestContext.set(resolvedToken, instance);
770
+ this.resolveDeferredProxies();
631
771
  return instance;
632
772
  }
633
773
 
@@ -635,12 +775,13 @@ export class CalyxContainer {
635
775
  return await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
636
776
  }
637
777
 
638
- if (targetRecord.instances.has(token)) {
639
- return targetRecord.instances.get(token);
778
+ if (targetRecord.instances.has(resolvedToken)) {
779
+ return targetRecord.instances.get(resolvedToken);
640
780
  }
641
781
 
642
782
  const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
643
- targetRecord.instances.set(token, instance);
783
+ targetRecord.instances.set(resolvedToken, instance);
784
+ this.resolveDeferredProxies();
644
785
  return instance;
645
786
  } finally {
646
787
  this.resolvingStack.pop();
@@ -700,7 +841,7 @@ export class CalyxContainer {
700
841
  const propertyInjects: Map<string | symbol, InjectionToken> =
701
842
  Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
702
843
  for (const [propertyKey, token] of propertyInjects.entries()) {
703
- instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, token, requestContext);
844
+ instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, resolveForwardRef(token), requestContext);
704
845
  }
705
846
 
706
847
  return instance;
@@ -865,125 +1006,170 @@ export class CalyxContainer {
865
1006
  return idx;
866
1007
  };
867
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
+
868
1044
  const instantiations: string[] = [];
869
1045
  const compiledTokens = new Set<string>();
1046
+ const compilingTokens = new Set<string>();
870
1047
 
871
1048
  const compileToken = (token: any, currentModuleClass: any): string => {
872
- if (token === REQUEST) {
1049
+ const resolvedToken = resolveForwardRef(token);
1050
+ if (resolvedToken === REQUEST) {
873
1051
  return `requestContext.get(c[${getClosureIdx(REQUEST)}])`;
874
1052
  }
875
- if (token === ModuleRef) {
1053
+ if (resolvedToken === ModuleRef) {
876
1054
  const record = this.modules.get(currentModuleClass)!;
877
1055
  return `c[${getClosureIdx(record.instances.get(ModuleRef))}]`;
878
1056
  }
879
1057
 
880
- const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
1058
+ const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
881
1059
  if (scope === Scope.DEFAULT) {
882
- const instance = this.resolveTokenInModuleContext(currentModuleClass, token);
1060
+ const instance = this.resolveTokenInModuleContext(currentModuleClass, resolvedToken);
883
1061
  return `c[${getClosureIdx(instance)}]`;
884
1062
  }
885
1063
 
886
- const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(token.name || token)}`;
1064
+ const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(resolvedToken.name || resolvedToken)}`;
887
1065
  if (compiledTokens.has(tokenKey)) {
888
- return `requestContext.get(c[${getClosureIdx(token)}])`;
1066
+ return `requestContext.get(c[${getClosureIdx(resolvedToken)}])`;
889
1067
  }
890
-
891
- const resolution = this.findProviderDefinition(currentModuleClass, token);
892
- if (!resolution) {
893
- const optional = this.isTokenOptional(token, currentModuleClass);
894
- 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)`;
895
1070
  }
896
1071
 
897
- const { targetModuleClass, provider } = resolution;
898
-
899
- let valExpr = '';
900
- if (typeof provider !== 'function' && 'useValue' in provider) {
901
- valExpr = `c[${getClosureIdx(provider.useValue)}]`;
902
- } else if (typeof provider !== 'function' && 'useExisting' in provider) {
903
- return compileToken(resolveForwardRef(provider.useExisting), targetModuleClass);
904
- } else {
905
- const ClassToInstantiate = typeof provider === 'function'
906
- ? provider
907
- : ('useClass' in provider ? provider.useClass : null);
908
-
909
- if (ClassToInstantiate) {
910
- const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', ClassToInstantiate) || [];
911
- const injectTokens: Map<number, any> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, ClassToInstantiate) || new Map();
912
- const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, ClassToInstantiate) || new Set();
913
-
914
- const argsExprs = paramTypes.map((paramType, i) => {
915
- const depToken = injectTokens.get(i) ?? paramType;
916
- const resolvedDepToken = resolveForwardRef(depToken);
917
- try {
918
- return compileToken(resolvedDepToken, targetModuleClass);
919
- } catch (err) {
920
- if (optionalParams.has(i)) {
921
- 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;
922
1107
  }
923
- 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};`);
924
1118
  }
925
- });
926
-
927
- const propertyInjects: Map<string | symbol, any> =
928
- Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, ClassToInstantiate) || new Map();
929
-
930
- const propAssignments: string[] = [];
931
- for (const [propKey, propToken] of propertyInjects.entries()) {
932
- const resolvedPropToken = resolveForwardRef(propToken);
933
- const propValExpr = compileToken(resolvedPropToken, targetModuleClass);
934
- propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
935
- }
936
1119
 
937
- const classIdx = getClosureIdx(ClassToInstantiate);
938
- const instantiateExpr = `new c[${classIdx}](${argsExprs.join(', ')})`;
939
-
940
- if (scope === Scope.REQUEST) {
941
- compiledTokens.add(tokenKey);
942
- const tokenIdx = getClosureIdx(token);
943
- instantiations.push(`
944
- if (!requestContext.has(c[${tokenIdx}])) {
945
- const inst = ${instantiateExpr};
946
- ${propAssignments.join('\n')}
947
- 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;
948
1145
  }
949
- `);
950
- valExpr = `requestContext.get(c[${tokenIdx}])`;
951
- } else {
952
- if (propAssignments.length > 0) {
953
- const tempVar = `transient_${compiledTokens.size}`;
954
- 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);
955
1157
  instantiations.push(`
956
- const ${tempVar} = ${instantiateExpr};
957
- ${propAssignments.map(line => line.replace('inst.', `${tempVar}.`)).join('\n')}
1158
+ if (!requestContext.has(c[${tokenIdx}])) {
1159
+ requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
1160
+ }
958
1161
  `);
959
- valExpr = tempVar;
1162
+ valExpr = `requestContext.get(c[${tokenIdx}])`;
960
1163
  } else {
961
- valExpr = instantiateExpr;
1164
+ valExpr = factoryCallExpr;
962
1165
  }
963
1166
  }
964
- } else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
965
- const injectTokens = provider.inject || [];
966
- const argsExprs = injectTokens.map((t) => compileToken(resolveForwardRef(t), targetModuleClass));
967
-
968
- const factoryIdx = getClosureIdx(provider.useFactory);
969
- const factoryCallExpr = `c[${factoryIdx}](${argsExprs.join(', ')})`;
970
-
971
- if (scope === Scope.REQUEST) {
972
- compiledTokens.add(tokenKey);
973
- const tokenIdx = getClosureIdx(token);
974
- instantiations.push(`
975
- if (!requestContext.has(c[${tokenIdx}])) {
976
- requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
977
- }
978
- `);
979
- valExpr = `requestContext.get(c[${tokenIdx}])`;
980
- } else {
981
- valExpr = factoryCallExpr;
982
- }
983
1167
  }
984
- }
985
1168
 
986
- return valExpr;
1169
+ return valExpr;
1170
+ } finally {
1171
+ compilingTokens.delete(tokenKey);
1172
+ }
987
1173
  };
988
1174
 
989
1175
  const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', controllerClass) || [];