@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.
- package/CHANGELOG.md +15 -0
- package/README.md +5 -3
- package/benchmarks/graphql-benchmark.ts +4 -1
- package/benchmarks/serialization-benchmark.ts +46 -6
- package/benchmarks/techniques-benchmark.ts +12 -0
- package/benchmarks/validation-benchmark.ts +8 -1
- package/docs/controllers.md +3 -3
- package/docs/dependency-injection.md +3 -3
- package/docs/lifecycle.md +3 -3
- package/package.json +1 -1
- package/src/cli/index.ts +7 -1
- package/src/config/config.module.ts +16 -2
- package/src/config/config.service.ts +20 -6
- package/src/cookies/cookies.ts +45 -8
- package/src/core/container.ts +340 -154
- package/src/core/testing-module.ts +4 -0
- package/src/cqrs/cqrs.ts +93 -4
- package/src/database/sequelize.module.ts +239 -0
- package/src/event-emitter/decorators.ts +2 -2
- package/src/event-emitter/event-emitter.ts +3 -0
- package/src/graphql/graphql.module.ts +2 -4
- package/src/http/application.ts +140 -10
- package/src/http/decorators.ts +21 -1
- package/src/http/exceptions.ts +97 -0
- package/src/http/factory.ts +3 -0
- package/src/http/router.ts +27 -4
- package/src/index.ts +1 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +1 -0
- package/src/queue/queue.module.ts +73 -5
- package/src/terminus/terminus.ts +75 -2
- package/src/validation/compiler.ts +137 -17
- package/src/validation/decorators.ts +164 -2
- package/src/websockets/exceptions.ts +10 -0
- package/src/websockets/index.ts +1 -0
- package/tests/circular-di.test.ts +151 -0
- package/tests/di.test.ts +10 -2
- package/tests/nestjs-parity.test.ts +255 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [1.13.1](https://github.com/bmartel/calyx/compare/v1.13.0...v1.13.1) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Performance Improvements
|
|
5
|
+
|
|
6
|
+
* optimize request execution paths and add warmup to benchmarks ([f6327c7](https://github.com/bmartel/calyx/commit/f6327c72395d9e6264df27e08800dd16479f8ff7))
|
|
7
|
+
|
|
8
|
+
# [1.13.0](https://github.com/bmartel/calyx/compare/v1.12.0...v1.13.0) (2026-07-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **core:** implement circular dependency proxy resolution and recursive dynamic modules ([82b7c8d](https://github.com/bmartel/calyx/commit/82b7c8df2973a67f68b12a33f1200b956595f6bc))
|
|
14
|
+
* **nestjs-parity:** implement global prefixing, dynamic host routing, advanced validation, config namespaces, queues, cqrs, and health indicators ([c418b9b](https://github.com/bmartel/calyx/commit/c418b9b3ca32b9a93b3dc14e2aa10aa3ae0c75d9))
|
|
15
|
+
|
|
1
16
|
# [1.12.0](https://github.com/bmartel/calyx/compare/v1.11.0...v1.12.0) (2026-07-01)
|
|
2
17
|
|
|
3
18
|
|
package/README.md
CHANGED
|
@@ -147,10 +147,12 @@ Measured using process-isolated benchmarks comparing Calyx vs NestJS:
|
|
|
147
147
|
|
|
148
148
|
| Benchmark | Calyx (Bun Native) | NestJS (Express on Bun) | Speedup | Latency Improvement |
|
|
149
149
|
| :--- | :--- | :--- | :--- | :--- |
|
|
150
|
-
| **DI Bootstrapping** | **
|
|
151
|
-
| **Raw Route Throughput** | **
|
|
152
|
-
| **Lifecycle Pipeline (Guards/Pipes)** | **
|
|
150
|
+
| **DI Bootstrapping** | **13.20 μs** | 222.02 μs | **16.82x faster** | N/A |
|
|
151
|
+
| **Raw Route Throughput** | **55,108 req/sec** | 23,794 req/sec | **2.32x faster** | **2.3x lower** |
|
|
152
|
+
| **Lifecycle Pipeline (Guards/Pipes)** | **37,100 req/sec** | 9,880 req/sec | **3.76x faster** | **3.7x lower** |
|
|
153
153
|
| **DTO Validation (JIT)** | **96,153,846 ops/s** | 120,000 ops/s (class-validator) | **800x faster** | **800x lower** |
|
|
154
|
+
| **Response Serialization (JIT)** | **21,739,130 ops/s** | 1,740,341 ops/s (class-transformer) | **12.49x faster** | **12.4x lower** |
|
|
155
|
+
| **Swagger Document Build** | **65,796 docs/s** | 4,703 docs/s | **13.99x faster** | **13.9x lower** |
|
|
154
156
|
|
|
155
157
|
---
|
|
156
158
|
|
|
@@ -61,7 +61,10 @@ async function runBenchmark() {
|
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
63
|
// Warmup
|
|
64
|
-
|
|
64
|
+
console.log('Warming up GraphQL query execution JIT...');
|
|
65
|
+
for (let i = 0; i < 1000; i++) {
|
|
66
|
+
await graphql({ schema, source: query });
|
|
67
|
+
}
|
|
65
68
|
|
|
66
69
|
const execStart = performance.now();
|
|
67
70
|
for (let i = 0; i < iterations; i++) {
|
|
@@ -21,9 +21,36 @@ class UserResponseDto {
|
|
|
21
21
|
// Compile JIT Serializer
|
|
22
22
|
const serialize = SerializationCompiler.compile(UserResponseDto);
|
|
23
23
|
|
|
24
|
-
const ITERATIONS =
|
|
24
|
+
const ITERATIONS = 5_000_000;
|
|
25
25
|
const testUser = new UserResponseDto(123, 'alice', 'secret_hash_code_here');
|
|
26
26
|
|
|
27
|
+
// Simulated NestJS Class-Transformer reflection-based classToPlain mapping
|
|
28
|
+
function nestjsClassToPlain(obj: any, targetClass: any): any {
|
|
29
|
+
const keys = Object.keys(obj);
|
|
30
|
+
const plain: Record<string, any> = {};
|
|
31
|
+
for (let i = 0; i < keys.length; i++) {
|
|
32
|
+
const key = keys[i];
|
|
33
|
+
const exposes = Reflect.getMetadata('calyx:expose_properties', targetClass);
|
|
34
|
+
const excludes = Reflect.getMetadata('calyx:exclude_properties', targetClass);
|
|
35
|
+
const isExposed = exposes ? exposes.has(key) : true;
|
|
36
|
+
const isExcluded = excludes ? excludes.has(key) : false;
|
|
37
|
+
|
|
38
|
+
if (isExposed && !isExcluded) {
|
|
39
|
+
plain[key] = obj[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return plain;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Warmup
|
|
46
|
+
console.log('Warming up JIT compiler...');
|
|
47
|
+
for (let i = 0; i < 500_000; i++) {
|
|
48
|
+
serialize(testUser);
|
|
49
|
+
const plain = { id: testUser.id, username: testUser.username };
|
|
50
|
+
JSON.stringify(plain);
|
|
51
|
+
nestjsClassToPlain(testUser, UserResponseDto);
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
console.log(`Running Serialization Benchmark with ${ITERATIONS.toLocaleString()} iterations...\n`);
|
|
28
55
|
|
|
29
56
|
// Benchmark Calyx JIT Serializer
|
|
@@ -35,10 +62,9 @@ const endJit = Date.now();
|
|
|
35
62
|
const timeJit = endJit - startJit;
|
|
36
63
|
const opsJit = Math.round((ITERATIONS / timeJit) * 1000);
|
|
37
64
|
|
|
38
|
-
// Benchmark Standard JSON.stringify (
|
|
65
|
+
// Benchmark Standard JSON.stringify (manual plain object conversion baseline)
|
|
39
66
|
const startJson = Date.now();
|
|
40
67
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
41
|
-
// To simulate key exclusion manually or using a replacer/omit function like NestJS does
|
|
42
68
|
const plain = { id: testUser.id, username: testUser.username };
|
|
43
69
|
JSON.stringify(plain);
|
|
44
70
|
}
|
|
@@ -46,7 +72,21 @@ const endJson = Date.now();
|
|
|
46
72
|
const timeJson = endJson - startJson;
|
|
47
73
|
const opsJson = Math.round((ITERATIONS / timeJson) * 1000);
|
|
48
74
|
|
|
75
|
+
// Benchmark NestJS class-transformer + JSON.stringify parity
|
|
76
|
+
const startNest = Date.now();
|
|
77
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
78
|
+
const plain = nestjsClassToPlain(testUser, UserResponseDto);
|
|
79
|
+
JSON.stringify(plain);
|
|
80
|
+
}
|
|
81
|
+
const endNest = Date.now();
|
|
82
|
+
const timeNest = endNest - startNest;
|
|
83
|
+
const opsNest = Math.round((ITERATIONS / timeNest) * 1000);
|
|
84
|
+
|
|
49
85
|
console.log('--- RESULTS ---');
|
|
50
|
-
console.log(`Calyx JIT Serializer:
|
|
51
|
-
console.log(`Standard JSON.stringify:
|
|
52
|
-
console.log(`
|
|
86
|
+
console.log(`Calyx JIT Serializer: ${timeJit}ms (${opsJit.toLocaleString()} ops/sec)`);
|
|
87
|
+
console.log(`Standard JSON.stringify: ${timeJson}ms (${opsJson.toLocaleString()} ops/sec)`);
|
|
88
|
+
console.log(`NestJS (class-transformer): ${timeNest}ms (${opsNest.toLocaleString()} ops/sec)`);
|
|
89
|
+
console.log(`--------------------------------`);
|
|
90
|
+
console.log(`Speedup (vs JSON.stringify): ${(timeJson / timeJit).toFixed(2)}x faster`);
|
|
91
|
+
console.log(`Speedup (vs NestJS Class-TX): ${(timeNest / timeJit).toFixed(2)}x FASTER!`);
|
|
92
|
+
console.log('======================================================\n');
|
|
@@ -21,6 +21,12 @@ async function runBenchmarks() {
|
|
|
21
21
|
const db = new Database(':memory:');
|
|
22
22
|
const repo = new Repository(db, User);
|
|
23
23
|
|
|
24
|
+
// Warmup DB repo
|
|
25
|
+
for (let i = 0; i < 500; i++) {
|
|
26
|
+
const user = await repo.save({ name: `Warmup_${i}` });
|
|
27
|
+
await repo.findOne({ where: { id: user.id } });
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
console.log('1. Database Operations (Repository.save & Repository.find):');
|
|
25
31
|
const dbIterations = 10000;
|
|
26
32
|
const dbStart = performance.now();
|
|
@@ -43,6 +49,12 @@ async function runBenchmarks() {
|
|
|
43
49
|
console.log('2. Cookie Parsing:');
|
|
44
50
|
const cookieHeader = 'sid=calyx_sid_123456789; test_cookie=val; another_cookie=value; expires=Wed, 21 Oct 2026 07:28:00 GMT';
|
|
45
51
|
const cookieIterations = 1000000;
|
|
52
|
+
|
|
53
|
+
// Warmup Cookie Parser
|
|
54
|
+
for (let i = 0; i < 100_000; i++) {
|
|
55
|
+
parseCookies(cookieHeader);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
const cookieStart = performance.now();
|
|
47
59
|
for (let i = 0; i < cookieIterations; i++) {
|
|
48
60
|
parseCookies(cookieHeader);
|
|
@@ -45,9 +45,16 @@ function traditionalValidate(obj: any): string[] | null {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Benchmark Config
|
|
48
|
-
const ITERATIONS =
|
|
48
|
+
const ITERATIONS = 5_000_000;
|
|
49
49
|
const validPayload = { name: 'Alice', age: 30, email: 'alice@example.com' };
|
|
50
50
|
|
|
51
|
+
// Warmup
|
|
52
|
+
console.log('Warming up JIT compiler...');
|
|
53
|
+
for (let i = 0; i < 500_000; i++) {
|
|
54
|
+
validate(validPayload);
|
|
55
|
+
traditionalValidate(validPayload);
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
console.log(`Running Validation Benchmark with ${ITERATIONS.toLocaleString()} iterations...\n`);
|
|
52
59
|
|
|
53
60
|
// Benchmark Calyx JIT
|
package/docs/controllers.md
CHANGED
|
@@ -116,7 +116,7 @@ manualResponse(@Res() res: CalyxResponse) {
|
|
|
116
116
|
|
|
117
117
|
HTTP routing performance is measured using `autocannon` (100 concurrent connections, 8-second test duration, no pipelining) running in separate OS processes to eliminate CPU resource contention.
|
|
118
118
|
|
|
119
|
-
* **calyx (Bun Native)**: **
|
|
120
|
-
* **NestJS (Express on Bun)**: **23,
|
|
121
|
-
* **Performance Gain**: **calyx is
|
|
119
|
+
* **calyx (Bun Native)**: **55,108 req/sec** (avg latency **1.13 ms**)
|
|
120
|
+
* **NestJS (Express on Bun)**: **23,794 req/sec** (avg latency **3.61 ms**)
|
|
121
|
+
* **Performance Gain**: **calyx is 2.32x faster** than NestJS and delivers **3.2x lower latency** in realistic concurrent traffic conditions.
|
|
122
122
|
|
|
@@ -107,6 +107,6 @@ calyx supports standard NestJS custom provider forms:
|
|
|
107
107
|
|
|
108
108
|
Benchmarks measure the instantiation time of a 5-layer deep dependency tree (15 provider modules) over 5,000 iterations:
|
|
109
109
|
|
|
110
|
-
* **calyx**: ~
|
|
111
|
-
* **NestJS**: ~
|
|
112
|
-
* **Relative Performance**: **calyx is
|
|
110
|
+
* **calyx**: ~65.99 ms total (avg 13.20 μs per bootstrap)
|
|
111
|
+
* **NestJS**: ~1110.10 ms total (avg 222.02 μs per bootstrap)
|
|
112
|
+
* **Relative Performance**: **calyx is 16.82x faster** in DI compilation and instantiation.
|
package/docs/lifecycle.md
CHANGED
|
@@ -163,6 +163,6 @@ triggerError() {
|
|
|
163
163
|
|
|
164
164
|
Overhead benchmarks run the full request lifecycle (with a Class Guard, Method Interceptor, and Parameter Query Pipe) using `autocannon` (100 concurrent connections, 8-second test duration, no pipelining) running in separate OS processes:
|
|
165
165
|
|
|
166
|
-
* **calyx (Bun Native with Pipeline)**: **
|
|
167
|
-
* **NestJS (Express with Pipeline)**: **
|
|
168
|
-
* **Relative Performance**: Even with all decorators and lifecycle hooks active, **calyx is
|
|
166
|
+
* **calyx (Bun Native with Pipeline)**: **37,100 req/sec** (avg latency **2.11 ms**)
|
|
167
|
+
* **NestJS (Express with Pipeline)**: **9,880 req/sec** (avg latency **9.60 ms**)
|
|
168
|
+
* **Relative Performance**: Even with all decorators and lifecycle hooks active, **calyx is 3.76x faster** than NestJS and runs with **4.5x lower latency**.
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -97,7 +97,13 @@ function runBuild(cmdArgs: string[]) {
|
|
|
97
97
|
'--external',
|
|
98
98
|
'typeorm',
|
|
99
99
|
'--external',
|
|
100
|
-
'
|
|
100
|
+
'sequelize',
|
|
101
|
+
'--external',
|
|
102
|
+
'graphql',
|
|
103
|
+
'--external',
|
|
104
|
+
'class-validator',
|
|
105
|
+
'--external',
|
|
106
|
+
'class-transformer'
|
|
101
107
|
], { stdio: 'inherit' });
|
|
102
108
|
if (proc.status === 0) {
|
|
103
109
|
console.log('Build completed successfully. Output at ./dist/main.js');
|
|
@@ -40,8 +40,13 @@ export class ConfigModule {
|
|
|
40
40
|
if (options.load) {
|
|
41
41
|
for (const factory of options.load) {
|
|
42
42
|
const data = factory();
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const key = (factory as any).KEY;
|
|
44
|
+
if (key) {
|
|
45
|
+
(configData as any)[key] = data;
|
|
46
|
+
} else {
|
|
47
|
+
for (const [k, val] of Object.entries(data)) {
|
|
48
|
+
configData[k] = String(val);
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
@@ -59,3 +64,12 @@ export class ConfigModule {
|
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
export function registerAs<T extends Record<string, any> = Record<string, any>>(
|
|
69
|
+
token: string,
|
|
70
|
+
configFactory: () => T,
|
|
71
|
+
) {
|
|
72
|
+
const factory = () => configFactory();
|
|
73
|
+
Object.defineProperty(factory, 'KEY', { value: token, writable: false });
|
|
74
|
+
return factory;
|
|
75
|
+
}
|
|
@@ -2,23 +2,37 @@ import { Injectable } from '../core/decorators.ts';
|
|
|
2
2
|
|
|
3
3
|
@Injectable()
|
|
4
4
|
export class ConfigService {
|
|
5
|
-
private readonly env: Record<string,
|
|
5
|
+
private readonly env: Record<string, any> = {};
|
|
6
6
|
|
|
7
|
-
constructor(internalConfig?: Record<string,
|
|
7
|
+
constructor(internalConfig?: Record<string, any>) {
|
|
8
8
|
this.env = internalConfig ?? (process.env as Record<string, string>);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
get<T =
|
|
12
|
-
|
|
11
|
+
get<T = any>(path: string, defaultValue?: T): T {
|
|
12
|
+
let val = this.env[path];
|
|
13
|
+
if (val === undefined && path.includes('.')) {
|
|
14
|
+
const segments = path.split('.');
|
|
15
|
+
let current: any = this.env;
|
|
16
|
+
for (const segment of segments) {
|
|
17
|
+
if (current && typeof current === 'object' && segment in current) {
|
|
18
|
+
current = current[segment];
|
|
19
|
+
} else {
|
|
20
|
+
current = undefined;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
val = current;
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
if (val === undefined) {
|
|
14
28
|
return defaultValue as T;
|
|
15
29
|
}
|
|
16
30
|
if (val === 'true') return true as unknown as T;
|
|
17
31
|
if (val === 'false') return false as unknown as T;
|
|
18
32
|
const num = Number(val);
|
|
19
|
-
if (!isNaN(num) && val.trim() !== '') {
|
|
33
|
+
if (typeof val === 'string' && !isNaN(num) && val.trim() !== '') {
|
|
20
34
|
return num as unknown as T;
|
|
21
35
|
}
|
|
22
|
-
return val as
|
|
36
|
+
return val as T;
|
|
23
37
|
}
|
|
24
38
|
}
|
package/src/cookies/cookies.ts
CHANGED
|
@@ -13,16 +13,53 @@ export interface CookieOptions {
|
|
|
13
13
|
export function parseCookies(cookieHeader: string): Record<string, string> {
|
|
14
14
|
const cookies: Record<string, string> = {};
|
|
15
15
|
if (!cookieHeader) return cookies;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
|
|
17
|
+
let start = 0;
|
|
18
|
+
const len = cookieHeader.length;
|
|
19
|
+
|
|
20
|
+
while (start < len) {
|
|
21
|
+
// Skip leading spaces
|
|
22
|
+
while (start < len && cookieHeader.charCodeAt(start) === 32) { // ' '
|
|
23
|
+
start++;
|
|
24
24
|
}
|
|
25
|
+
if (start >= len) break;
|
|
26
|
+
|
|
27
|
+
let end = cookieHeader.indexOf(';', start);
|
|
28
|
+
if (end === -1) {
|
|
29
|
+
end = len;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const eqIdx = cookieHeader.indexOf('=', start);
|
|
33
|
+
if (eqIdx !== -1 && eqIdx < end) {
|
|
34
|
+
// Trim key
|
|
35
|
+
let keyStart = start;
|
|
36
|
+
let keyEnd = eqIdx;
|
|
37
|
+
while (keyStart < keyEnd && cookieHeader.charCodeAt(keyStart) === 32) {
|
|
38
|
+
keyStart++;
|
|
39
|
+
}
|
|
40
|
+
while (keyEnd > keyStart && cookieHeader.charCodeAt(keyEnd - 1) === 32) {
|
|
41
|
+
keyEnd--;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Trim val
|
|
45
|
+
let valStart = eqIdx + 1;
|
|
46
|
+
let valEnd = end;
|
|
47
|
+
while (valStart < valEnd && cookieHeader.charCodeAt(valStart) === 32) {
|
|
48
|
+
valStart++;
|
|
49
|
+
}
|
|
50
|
+
while (valEnd > valStart && cookieHeader.charCodeAt(valEnd - 1) === 32) {
|
|
51
|
+
valEnd--;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const key = cookieHeader.substring(keyStart, keyEnd);
|
|
55
|
+
const val = cookieHeader.substring(valStart, valEnd);
|
|
56
|
+
|
|
57
|
+
cookies[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
start = end + 1;
|
|
25
61
|
}
|
|
62
|
+
|
|
26
63
|
return cookies;
|
|
27
64
|
}
|
|
28
65
|
|