@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
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** | **39.12 μs** | 259.83 μs | **6.64x faster** | N/A |
151
- | **Raw Route Throughput** | **54,764 req/sec** | 18,759 req/sec | **2.92x faster** | **2.9x lower** |
152
- | **Lifecycle Pipeline (Guards/Pipes)** | **41,683 req/sec** | 8,803 req/sec | **4.73x faster** | **4.7x lower** |
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
- await graphql({ schema, source: query });
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 = 10_000_000;
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 (simulating traditional serialization)
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: ${timeJit}ms (${opsJit.toLocaleString()} ops/sec)`);
51
- console.log(`Standard JSON.stringify: ${timeJson}ms (${opsJson.toLocaleString()} ops/sec)`);
52
- console.log(`Speedup Factor: ${(timeJson / timeJit).toFixed(2)}x faster\n`);
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 = 10_000_000;
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
@@ -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)**: **62,164 req/sec** (avg latency **1.05 ms**)
120
- * **NestJS (Express on Bun)**: **23,694 req/sec** (avg latency **3.66 ms**)
121
- * **Performance Gain**: **calyx is ~2.6x faster** than NestJS and delivers **3.5x lower latency** in realistic concurrent traffic conditions.
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**: ~41.45 ms total (avg 8.29 μs per bootstrap)
111
- * **NestJS**: ~1104.36 ms total (avg 220.87 μs per bootstrap)
112
- * **Relative Performance**: **calyx is ~26x faster** in DI compilation and instantiation.
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)**: **25,152 req/sec** (avg latency **3.25 ms**)
167
- * **NestJS (Express with Pipeline)**: **10,274 req/sec** (avg latency **9.28 ms**)
168
- * **Relative Performance**: Even with all decorators and lifecycle hooks active, **calyx is ~2.4x faster** than NestJS and runs with **3x lower latency**.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martel/calyx",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "description": "High-performance Bun-native NestJS-compatible framework",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -97,7 +97,13 @@ function runBuild(cmdArgs: string[]) {
97
97
  '--external',
98
98
  'typeorm',
99
99
  '--external',
100
- 'graphql'
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
- for (const [key, val] of Object.entries(data)) {
44
- configData[key] = String(val);
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, string> = {};
5
+ private readonly env: Record<string, any> = {};
6
6
 
7
- constructor(internalConfig?: Record<string, 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 = string>(path: string, defaultValue?: T): T {
12
- const val = this.env[path];
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 unknown as T;
36
+ return val as T;
23
37
  }
24
38
  }
@@ -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
- const parts = cookieHeader.split(';');
17
- for (let i = 0; i < parts.length; i++) {
18
- const part = parts[i];
19
- const eqIdx = part.indexOf('=');
20
- if (eqIdx !== -1) {
21
- const key = part.substring(0, eqIdx).trim();
22
- const val = part.substring(eqIdx + 1).trim();
23
- cookies[key] = decodeURIComponent(val);
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