@martel/calyx 1.13.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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  # [1.13.0](https://github.com/bmartel/calyx/compare/v1.12.0...v1.13.0) (2026-07-01)
2
9
 
3
10
 
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.13.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": {
@@ -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
 
@@ -1,5 +1,7 @@
1
1
  import { CalyxContainer, DynamicModule } from '../core/container.ts';
2
2
  import { Module } from '../core/decorators.ts';
3
+ import { CalyxExecutionContext } from '../lifecycle/context.ts';
4
+ import { HttpException } from '../http/exceptions.ts';
3
5
  import {
4
6
  GraphQLSchema,
5
7
  GraphQLObjectType,
@@ -262,7 +264,6 @@ export class GraphQLModule {
262
264
 
263
265
  const resolveFn = async (parent: any, args: any, context: any, info: any) => {
264
266
  const req = context?.req;
265
- const { CalyxExecutionContext } = await import('../lifecycle/context.ts');
266
267
  const execContext = new CalyxExecutionContext(req, null, resolverClass, resolverInstance[fieldMeta.propertyKey]);
267
268
  (execContext as any).type = 'graphql';
268
269
  (execContext as any).data = args;
@@ -277,7 +278,6 @@ export class GraphQLModule {
277
278
  const canActivate = guard.instance.canActivate(execContext);
278
279
  const resolved = canActivate instanceof Promise ? await canActivate : canActivate;
279
280
  if (!resolved) {
280
- const { HttpException } = await import('../http/exceptions.ts');
281
281
  throw new HttpException('Forbidden resource', 403);
282
282
  }
283
283
  }
@@ -429,7 +429,6 @@ export class GraphQLModule {
429
429
 
430
430
  targetFields[fieldName].resolve = async (parent: any, args: any, context: any, info: any) => {
431
431
  const req = context?.req;
432
- const { CalyxExecutionContext } = await import('../lifecycle/context.ts');
433
432
  const execContext = new CalyxExecutionContext(req, null, resolverClass, resolverInstance[fieldRes.propertyKey]);
434
433
  (execContext as any).type = 'graphql';
435
434
  (execContext as any).data = args;
@@ -443,7 +442,6 @@ export class GraphQLModule {
443
442
  const canActivate = guard.instance.canActivate(execContext);
444
443
  const resolved = canActivate instanceof Promise ? await canActivate : canActivate;
445
444
  if (!resolved) {
446
- const { HttpException } = await import('../http/exceptions.ts');
447
445
  throw new HttpException('Forbidden resource', 403);
448
446
  }
449
447
  }
@@ -158,6 +158,7 @@ export class CalyxApplication {
158
158
  private serverPort = 3000;
159
159
  private graphqlSchema: any = null;
160
160
  private graphqlQueryCache = new Map<string, any>();
161
+ private graphqlLib: any = null;
161
162
  private isInitialized = false;
162
163
  private versioningOptions?: VersioningOptions;
163
164
  private globalPrefix?: string;
@@ -578,8 +579,8 @@ export class CalyxApplication {
578
579
  try {
579
580
  const urlStr = req.url;
580
581
  // Parse cookies
581
- const cookieHeader = req.headers.get('cookie') || '';
582
- (req as any).cookies = parseCookies(cookieHeader);
582
+ const cookieHeader = req.headers.get('cookie');
583
+ (req as any).cookies = cookieHeader ? parseCookies(cookieHeader) : {};
583
584
 
584
585
  // Fast path parsing
585
586
  let pathname = '/';
@@ -1345,7 +1346,7 @@ export class CalyxApplication {
1345
1346
  const body = await req.json() as any;
1346
1347
  const { query, variables } = body;
1347
1348
 
1348
- const { parse, validate, execute } = await import('graphql');
1349
+ const { parse, validate, execute } = this.graphqlLib || (this.graphqlLib = await import('graphql'));
1349
1350
 
1350
1351
  let document = this.graphqlQueryCache.get(query);
1351
1352
  if (!document) {
@@ -1480,7 +1481,7 @@ export class CalyxApplication {
1480
1481
  const { id, payload } = data;
1481
1482
  const { query, variables } = payload;
1482
1483
 
1483
- const { subscribe, parse, validate } = await import('graphql');
1484
+ const { subscribe, parse, validate } = this.graphqlLib || (this.graphqlLib = await import('graphql'));
1484
1485
 
1485
1486
  let document = this.graphqlQueryCache.get(query);
1486
1487
  if (!document) {
@@ -215,15 +215,12 @@ export class SerializationCompiler {
215
215
  const jsonParts: string[] = [];
216
216
  for (const key of keys) {
217
217
  const propRules = rules.filter((r) => r.propertyKey === key);
218
- const isNumber = propRules.some((r) => r.type === 'number');
219
- const isString = propRules.some((r) => r.type === 'string');
218
+ const designType = Reflect.getMetadata('design:type', dtoClass.prototype, key);
219
+ const isNumber = propRules.some((r) => r.type === 'number') || designType === Number;
220
+ const isBoolean = propRules.some((r) => r.type === 'isBoolean') || designType === Boolean;
220
221
 
221
- if (isNumber) {
222
+ if (isNumber || isBoolean) {
222
223
  jsonParts.push(`"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : obj.${key}}`);
223
- } else if (isString) {
224
- jsonParts.push(
225
- `"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : JSON.stringify(obj.${key})}`
226
- );
227
224
  } else {
228
225
  jsonParts.push(
229
226
  `"${key}":\${obj.${key} === undefined || obj.${key} === null ? null : JSON.stringify(obj.${key})}`