@martel/calyx 1.3.0 → 1.4.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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/core/container.ts +220 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/bmartel/calyx/compare/v1.3.0...v1.4.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **di:** implement JIT request-scoped DI factory compiler at bootstrap ([339c069](https://github.com/bmartel/calyx/commit/339c0693bdf2afe64f89648012da4fad55d91444))
|
|
7
|
+
|
|
1
8
|
# [1.3.0](https://github.com/bmartel/calyx/compare/v1.2.4...v1.3.0) (2026-07-01)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
package/src/core/container.ts
CHANGED
|
@@ -64,6 +64,7 @@ export class CalyxContainer {
|
|
|
64
64
|
private resolvingStack: { moduleClass: any; token: InjectionToken }[] = [];
|
|
65
65
|
private providerScopes = new Map<InjectionToken, Scope>();
|
|
66
66
|
private controllerScopes = new Map<any, Scope>();
|
|
67
|
+
private compiledControllerFactories = new Map<any, (requestContext: Map<any, any>) => any>();
|
|
67
68
|
|
|
68
69
|
getModuleRecord(moduleClass: any): ModuleRecord | undefined {
|
|
69
70
|
return this.modules.get(resolveForwardRef(moduleClass));
|
|
@@ -447,6 +448,8 @@ export class CalyxContainer {
|
|
|
447
448
|
record.instances.set(moduleClass, instance);
|
|
448
449
|
}
|
|
449
450
|
}
|
|
451
|
+
|
|
452
|
+
this.compileControllerFactories();
|
|
450
453
|
}
|
|
451
454
|
|
|
452
455
|
private resolveController(moduleClass: any, controllerClass: any): any {
|
|
@@ -478,6 +481,12 @@ export class CalyxContainer {
|
|
|
478
481
|
if (requestContext.has(controllerClass)) {
|
|
479
482
|
return requestContext.get(controllerClass);
|
|
480
483
|
}
|
|
484
|
+
const factory = this.compiledControllerFactories.get(controllerClass);
|
|
485
|
+
if (factory) {
|
|
486
|
+
const instance = factory(requestContext);
|
|
487
|
+
requestContext.set(controllerClass, instance);
|
|
488
|
+
return instance;
|
|
489
|
+
}
|
|
481
490
|
const instance = this.instantiateClass(controllerClass, moduleClass, requestContext);
|
|
482
491
|
requestContext.set(controllerClass, instance);
|
|
483
492
|
return instance;
|
|
@@ -609,4 +618,215 @@ export class CalyxContainer {
|
|
|
609
618
|
}
|
|
610
619
|
}
|
|
611
620
|
}
|
|
621
|
+
|
|
622
|
+
private compileControllerFactories() {
|
|
623
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
624
|
+
for (const controllerClass of record.controllers) {
|
|
625
|
+
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
626
|
+
if (scope === Scope.REQUEST) {
|
|
627
|
+
const closureValues: any[] = [];
|
|
628
|
+
const getClosureIdx = (val: any): number => {
|
|
629
|
+
let idx = closureValues.indexOf(val);
|
|
630
|
+
if (idx === -1) {
|
|
631
|
+
idx = closureValues.length;
|
|
632
|
+
closureValues.push(val);
|
|
633
|
+
}
|
|
634
|
+
return idx;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const instantiations: string[] = [];
|
|
638
|
+
const compiledTokens = new Set<string>();
|
|
639
|
+
|
|
640
|
+
const compileToken = (token: any, currentModuleClass: any): string => {
|
|
641
|
+
if (token === REQUEST) {
|
|
642
|
+
return `requestContext.get(c[${getClosureIdx(REQUEST)}])`;
|
|
643
|
+
}
|
|
644
|
+
if (token === ModuleRef) {
|
|
645
|
+
const record = this.modules.get(currentModuleClass)!;
|
|
646
|
+
return `c[${getClosureIdx(record.instances.get(ModuleRef))}]`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
650
|
+
if (scope === Scope.DEFAULT) {
|
|
651
|
+
const instance = this.resolveTokenInModuleContext(currentModuleClass, token);
|
|
652
|
+
return `c[${getClosureIdx(instance)}]`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(token.name || token)}`;
|
|
656
|
+
if (compiledTokens.has(tokenKey)) {
|
|
657
|
+
return `requestContext.get(c[${getClosureIdx(token)}])`;
|
|
658
|
+
}
|
|
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)}])); })()`;
|
|
664
|
+
}
|
|
665
|
+
|
|
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`;
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
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
|
+
|
|
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);
|
|
715
|
+
}
|
|
716
|
+
`);
|
|
717
|
+
valExpr = `requestContext.get(c[${tokenIdx}])`;
|
|
718
|
+
} else {
|
|
719
|
+
if (propAssignments.length > 0) {
|
|
720
|
+
const tempVar = `transient_${compiledTokens.size}`;
|
|
721
|
+
compiledTokens.add(tempVar);
|
|
722
|
+
instantiations.push(`
|
|
723
|
+
const ${tempVar} = ${instantiateExpr};
|
|
724
|
+
${propAssignments.map(line => line.replace('inst.', `${tempVar}.`)).join('\n')}
|
|
725
|
+
`);
|
|
726
|
+
valExpr = tempVar;
|
|
727
|
+
} else {
|
|
728
|
+
valExpr = instantiateExpr;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
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
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return valExpr;
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', controllerClass) || [];
|
|
757
|
+
const injectTokens: Map<number, any> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, controllerClass) || new Map();
|
|
758
|
+
const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, controllerClass) || new Set();
|
|
759
|
+
|
|
760
|
+
const argsExprs = paramTypes.map((paramType, i) => {
|
|
761
|
+
const depToken = injectTokens.get(i) ?? paramType;
|
|
762
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
763
|
+
try {
|
|
764
|
+
return compileToken(resolvedDepToken, moduleClass);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
if (optionalParams.has(i)) {
|
|
767
|
+
return `undefined`;
|
|
768
|
+
}
|
|
769
|
+
throw err;
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const propertyInjects: Map<string | symbol, any> =
|
|
774
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, controllerClass) || new Map();
|
|
775
|
+
|
|
776
|
+
const propAssignments: string[] = [];
|
|
777
|
+
for (const [propKey, propToken] of propertyInjects.entries()) {
|
|
778
|
+
const resolvedPropToken = resolveForwardRef(propToken);
|
|
779
|
+
const propValExpr = compileToken(resolvedPropToken, moduleClass);
|
|
780
|
+
propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const controllerClassIdx = getClosureIdx(controllerClass);
|
|
784
|
+
const instantiateControllerExpr = `new c[${controllerClassIdx}](${argsExprs.join(', ')})`;
|
|
785
|
+
|
|
786
|
+
const factoryBody = `
|
|
787
|
+
${instantiations.join('\n')}
|
|
788
|
+
const inst = ${instantiateControllerExpr};
|
|
789
|
+
${propAssignments.join('\n')}
|
|
790
|
+
return inst;
|
|
791
|
+
`;
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const factoryFn = new Function('c', `
|
|
795
|
+
return function resolve(requestContext) {
|
|
796
|
+
${factoryBody}
|
|
797
|
+
}
|
|
798
|
+
`)(closureValues);
|
|
799
|
+
this.compiledControllerFactories.set(controllerClass, factoryFn);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
console.error('Failed to compile JIT DI Factory for controller:', controllerClass.name || controllerClass);
|
|
802
|
+
console.error('Factory body was:', factoryBody);
|
|
803
|
+
throw err;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private isTokenOptional(token: any, moduleClass: any): boolean {
|
|
811
|
+
for (const record of this.modules.values()) {
|
|
812
|
+
for (const provider of record.providers.values()) {
|
|
813
|
+
const Class = typeof provider === 'function'
|
|
814
|
+
? provider
|
|
815
|
+
: ('useClass' in provider ? provider.useClass : null);
|
|
816
|
+
if (Class) {
|
|
817
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', Class) || [];
|
|
818
|
+
const injectTokens = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, Class) || new Map();
|
|
819
|
+
const optionalParams = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, Class) || new Set();
|
|
820
|
+
for (let i = 0; i < paramTypes.length; i++) {
|
|
821
|
+
const pToken = injectTokens.get(i) ?? paramTypes[i];
|
|
822
|
+
if (resolveForwardRef(pToken) === token && optionalParams.has(i)) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
612
831
|
}
|
|
832
|
+
|