@martel/calyx 0.1.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/.github/workflows/release.yml +39 -0
- package/.releaserc.json +27 -0
- package/benchmarks/di-benchmark.ts +114 -0
- package/benchmarks/http-benchmark.ts +103 -0
- package/benchmarks/lifecycle-benchmark.ts +102 -0
- package/benchmarks/run-calyx-lifecycle.ts +58 -0
- package/benchmarks/run-calyx.ts +23 -0
- package/benchmarks/run-nest-lifecycle.ts +60 -0
- package/benchmarks/run-nest.ts +24 -0
- package/docs/controllers.md +122 -0
- package/docs/dependency-injection.md +112 -0
- package/docs/lifecycle.md +168 -0
- package/docs/migration.md +108 -0
- package/package.json +31 -0
- package/src/core/container.ts +387 -0
- package/src/core/decorators.ts +45 -0
- package/src/core/index.ts +4 -0
- package/src/core/metadata.ts +61 -0
- package/src/core/module-ref.ts +5 -0
- package/src/http/application.ts +588 -0
- package/src/http/decorators.ts +103 -0
- package/src/http/exceptions.ts +47 -0
- package/src/http/factory.ts +8 -0
- package/src/http/index.ts +5 -0
- package/src/http/router.ts +97 -0
- package/src/index.ts +4 -0
- package/src/lifecycle/context.ts +41 -0
- package/src/lifecycle/decorators.ts +37 -0
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/interfaces.ts +49 -0
- package/tests/di.test.ts +283 -0
- package/tests/dynamic-module.test.ts +53 -0
- package/tests/lifecycle.test.ts +169 -0
- package/tests/routing.test.ts +155 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
name: Release
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
issues: write
|
|
15
|
+
pull-requests: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
persist-credentials: false
|
|
23
|
+
|
|
24
|
+
- name: Setup Bun
|
|
25
|
+
uses: oven-sh/setup-bun@v2
|
|
26
|
+
with:
|
|
27
|
+
bun-version: latest
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: bun install --frozen-lockfile
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: bun test
|
|
34
|
+
|
|
35
|
+
- name: Run semantic-release
|
|
36
|
+
env:
|
|
37
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
38
|
+
NPM_TOKEN: 'OIDC' # Dummy token to bypass semantic-release check; npm CLI uses the OIDC id-token
|
|
39
|
+
run: bunx semantic-release
|
package/.releaserc.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": [
|
|
3
|
+
"main"
|
|
4
|
+
],
|
|
5
|
+
"plugins": [
|
|
6
|
+
"@semantic-release/commit-analyzer",
|
|
7
|
+
"@semantic-release/release-notes-generator",
|
|
8
|
+
"@semantic-release/changelog",
|
|
9
|
+
[
|
|
10
|
+
"@semantic-release/npm",
|
|
11
|
+
{
|
|
12
|
+
"npmPublish": true
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
"@semantic-release/git",
|
|
17
|
+
{
|
|
18
|
+
"assets": [
|
|
19
|
+
"package.json",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"@semantic-release/github"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
// Import NestJS
|
|
4
|
+
import { Module as NestModule, Injectable as NestInjectable } from '@nestjs/common';
|
|
5
|
+
import { NestFactory } from '@nestjs/core';
|
|
6
|
+
// Import calyx
|
|
7
|
+
import { Module as calyxModule, Injectable as calyxInjectable, calyxContainer } from '../src/index.ts';
|
|
8
|
+
|
|
9
|
+
// ----------------------------------------------------
|
|
10
|
+
// Setup Dependency Tree classes
|
|
11
|
+
// ----------------------------------------------------
|
|
12
|
+
|
|
13
|
+
// calyx classes
|
|
14
|
+
@calyxInjectable()
|
|
15
|
+
class calyxService5 {}
|
|
16
|
+
|
|
17
|
+
@calyxInjectable()
|
|
18
|
+
class calyxService4 {
|
|
19
|
+
constructor(public s5: calyxService5) {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@calyxInjectable()
|
|
23
|
+
class calyxService3 {
|
|
24
|
+
constructor(public s4: calyxService4) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@calyxInjectable()
|
|
28
|
+
class calyxService2 {
|
|
29
|
+
constructor(public s3: calyxService3) {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@calyxInjectable()
|
|
33
|
+
class calyxService1 {
|
|
34
|
+
constructor(public s2: calyxService2) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@calyxModule({
|
|
38
|
+
providers: [calyxService1, calyxService2, calyxService3, calyxService4, calyxService5],
|
|
39
|
+
})
|
|
40
|
+
class calyxAppModule {}
|
|
41
|
+
|
|
42
|
+
// NestJS classes
|
|
43
|
+
@NestInjectable()
|
|
44
|
+
class NestService5 {}
|
|
45
|
+
|
|
46
|
+
@NestInjectable()
|
|
47
|
+
class NestService4 {
|
|
48
|
+
constructor(public s5: NestService5) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@NestInjectable()
|
|
52
|
+
class NestService3 {
|
|
53
|
+
constructor(public s4: NestService4) {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@NestInjectable()
|
|
57
|
+
class NestService2 {
|
|
58
|
+
constructor(public s3: NestService3) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@NestInjectable()
|
|
62
|
+
class NestService1 {
|
|
63
|
+
constructor(public s2: NestService2) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@NestModule({
|
|
67
|
+
providers: [NestService1, NestService2, NestService3, NestService4, NestService5],
|
|
68
|
+
})
|
|
69
|
+
class NestAppModule {}
|
|
70
|
+
|
|
71
|
+
// ----------------------------------------------------
|
|
72
|
+
// Benchmarking Logic
|
|
73
|
+
// ----------------------------------------------------
|
|
74
|
+
|
|
75
|
+
async function runBenchmark() {
|
|
76
|
+
console.log('=============================================');
|
|
77
|
+
console.log('calyx vs NestJS DI Container Benchmark');
|
|
78
|
+
console.log('=============================================');
|
|
79
|
+
|
|
80
|
+
const iterations = 5000;
|
|
81
|
+
|
|
82
|
+
// 1. Benchmark calyx Bootstrap
|
|
83
|
+
console.log(`Bootstrapping calyx container ${iterations} times...`);
|
|
84
|
+
const calyxStart = performance.now();
|
|
85
|
+
for (let i = 0; i < iterations; i++) {
|
|
86
|
+
const container = new calyxContainer();
|
|
87
|
+
container.bootstrap(calyxAppModule);
|
|
88
|
+
// Resolve one
|
|
89
|
+
container.getGlobalOrAnyInstance(calyxService1);
|
|
90
|
+
}
|
|
91
|
+
const calyxEnd = performance.now();
|
|
92
|
+
const calyxTime = calyxEnd - calyxStart;
|
|
93
|
+
console.log(`calyx: ${calyxTime.toFixed(2)} ms (avg ${(calyxTime / iterations * 1000).toFixed(2)} μs per bootstrap)`);
|
|
94
|
+
|
|
95
|
+
// 2. Benchmark NestJS Bootstrap
|
|
96
|
+
console.log(`Bootstrapping NestJS container ${iterations} times...`);
|
|
97
|
+
const nestStart = performance.now();
|
|
98
|
+
for (let i = 0; i < iterations; i++) {
|
|
99
|
+
const app = await NestFactory.createApplicationContext(NestAppModule, { logger: false });
|
|
100
|
+
// Resolve one
|
|
101
|
+
app.get(NestService1);
|
|
102
|
+
await app.close();
|
|
103
|
+
}
|
|
104
|
+
const nestEnd = performance.now();
|
|
105
|
+
const nestTime = nestEnd - nestStart;
|
|
106
|
+
console.log(`NestJS: ${nestTime.toFixed(2)} ms (avg ${(nestTime / iterations * 1000).toFixed(2)} μs per bootstrap)`);
|
|
107
|
+
|
|
108
|
+
console.log('---------------------------------------------');
|
|
109
|
+
const speedup = nestTime / calyxTime;
|
|
110
|
+
console.log(`calyx is ${speedup.toFixed(2)}x FASTER than NestJS in Dependency Injection bootstrapping.`);
|
|
111
|
+
console.log('=============================================');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
runBenchmark().catch(console.error);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import autocannon from 'autocannon';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
async function runAutocannon(port: number, name: string): Promise<autocannon.Result> {
|
|
5
|
+
console.log(`Running autocannon benchmark against ${name}...`);
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
autocannon(
|
|
8
|
+
{
|
|
9
|
+
url: `http://localhost:${port}/hello`,
|
|
10
|
+
connections: 100,
|
|
11
|
+
pipelining: 1,
|
|
12
|
+
duration: 8, // 8 seconds to get stable results
|
|
13
|
+
},
|
|
14
|
+
(err, result) => {
|
|
15
|
+
if (err) reject(err);
|
|
16
|
+
else resolve(result);
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const calyxPath = join(import.meta.dir, 'run-calyx.ts');
|
|
24
|
+
const nestPath = join(import.meta.dir, 'run-nest.ts');
|
|
25
|
+
|
|
26
|
+
console.log('Spawning calyx server...');
|
|
27
|
+
const calyxProc = Bun.spawn(['bun', calyxPath], {
|
|
28
|
+
stdout: 'pipe',
|
|
29
|
+
stderr: 'inherit',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Wait for calyx server to print ready message
|
|
33
|
+
const calyxReader = calyxProc.stdout.getReader();
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
while (true) {
|
|
36
|
+
const { value, done } = await calyxReader.read();
|
|
37
|
+
if (done) break;
|
|
38
|
+
const chunk = decoder.decode(value);
|
|
39
|
+
if (chunk.includes('calyx listening')) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
calyxReader.releaseLock();
|
|
44
|
+
console.log('calyx server is ready.');
|
|
45
|
+
|
|
46
|
+
console.log('Spawning NestJS server...');
|
|
47
|
+
const nestProc = Bun.spawn(['bun', nestPath], {
|
|
48
|
+
stdout: 'pipe',
|
|
49
|
+
stderr: 'inherit',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const nestReader = nestProc.stdout.getReader();
|
|
53
|
+
while (true) {
|
|
54
|
+
const { value, done } = await nestReader.read();
|
|
55
|
+
if (done) break;
|
|
56
|
+
const chunk = decoder.decode(value);
|
|
57
|
+
if (chunk.includes('NestJS listening')) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
nestReader.releaseLock();
|
|
62
|
+
console.log('NestJS server is ready.');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Warmup
|
|
66
|
+
console.log('Warming up connections...');
|
|
67
|
+
for (let i = 0; i < 50; i++) {
|
|
68
|
+
await fetch('http://localhost:4001/hello');
|
|
69
|
+
await fetch('http://localhost:4002/hello');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Run benchmarks
|
|
73
|
+
const calyxResult = await runAutocannon(4001, 'calyx (Bun Native, separate process)');
|
|
74
|
+
const nestResult = await runAutocannon(4002, 'NestJS (Express on Bun, separate process)');
|
|
75
|
+
|
|
76
|
+
console.log('\n======================================================');
|
|
77
|
+
console.log('HTTP Throughput & Latency Comparison (Multi-Process)');
|
|
78
|
+
console.log('======================================================');
|
|
79
|
+
console.log(`calyx (Bun Native):`);
|
|
80
|
+
console.log(` Requests/Sec (Avg): ${calyxResult.requests.average.toFixed(0)}`);
|
|
81
|
+
console.log(` Latency (Avg ms): ${calyxResult.latency.average.toFixed(2)}`);
|
|
82
|
+
console.log(` Throughput (Mb/s): ${(calyxResult.throughput.average / 1024 / 1024).toFixed(2)}`);
|
|
83
|
+
console.log(`------------------------------------------------------`);
|
|
84
|
+
console.log(`NestJS (Express on Bun):`);
|
|
85
|
+
console.log(` Requests/Sec (Avg): ${nestResult.requests.average.toFixed(0)}`);
|
|
86
|
+
console.log(` Latency (Avg ms): ${nestResult.latency.average.toFixed(2)}`);
|
|
87
|
+
console.log(` Throughput (Mb/s): ${(nestResult.throughput.average / 1024 / 1024).toFixed(2)}`);
|
|
88
|
+
console.log(`------------------------------------------------------`);
|
|
89
|
+
|
|
90
|
+
const speedup = calyxResult.requests.average / nestResult.requests.average;
|
|
91
|
+
console.log(`calyx is ${speedup.toFixed(2)}x FASTER in requests/sec than NestJS!`);
|
|
92
|
+
console.log('======================================================\n');
|
|
93
|
+
|
|
94
|
+
} finally {
|
|
95
|
+
// Kill processes
|
|
96
|
+
console.log('Shutting down server processes...');
|
|
97
|
+
calyxProc.kill();
|
|
98
|
+
nestProc.kill();
|
|
99
|
+
console.log('Servers shut down successfully.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import autocannon from 'autocannon';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
async function runAutocannon(port: number, name: string): Promise<autocannon.Result> {
|
|
5
|
+
console.log(`Running autocannon benchmark against ${name}...`);
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
autocannon(
|
|
8
|
+
{
|
|
9
|
+
url: `http://localhost:${port}/hello?num=42`,
|
|
10
|
+
connections: 100,
|
|
11
|
+
pipelining: 1,
|
|
12
|
+
duration: 8, // 8 seconds to get stable results
|
|
13
|
+
},
|
|
14
|
+
(err, result) => {
|
|
15
|
+
if (err) reject(err);
|
|
16
|
+
else resolve(result);
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const calyxPath = join(import.meta.dir, 'run-calyx-lifecycle.ts');
|
|
24
|
+
const nestPath = join(import.meta.dir, 'run-nest-lifecycle.ts');
|
|
25
|
+
|
|
26
|
+
console.log('Spawning calyx lifecycle server...');
|
|
27
|
+
const calyxProc = Bun.spawn(['bun', calyxPath], {
|
|
28
|
+
stdout: 'pipe',
|
|
29
|
+
stderr: 'inherit',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
const calyxReader = calyxProc.stdout.getReader();
|
|
34
|
+
while (true) {
|
|
35
|
+
const { value, done } = await calyxReader.read();
|
|
36
|
+
if (done) break;
|
|
37
|
+
const chunk = decoder.decode(value);
|
|
38
|
+
if (chunk.includes('calyx lifecycle listening')) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
calyxReader.releaseLock();
|
|
43
|
+
console.log('calyx lifecycle server is ready.');
|
|
44
|
+
|
|
45
|
+
console.log('Spawning NestJS lifecycle server...');
|
|
46
|
+
const nestProc = Bun.spawn(['bun', nestPath], {
|
|
47
|
+
stdout: 'pipe',
|
|
48
|
+
stderr: 'inherit',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const nestReader = nestProc.stdout.getReader();
|
|
52
|
+
while (true) {
|
|
53
|
+
const { value, done } = await nestReader.read();
|
|
54
|
+
if (done) break;
|
|
55
|
+
const chunk = decoder.decode(value);
|
|
56
|
+
if (chunk.includes('NestJS lifecycle listening')) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
nestReader.releaseLock();
|
|
61
|
+
console.log('NestJS lifecycle server is ready.');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Warmup
|
|
65
|
+
console.log('Warming up connections...');
|
|
66
|
+
for (let i = 0; i < 50; i++) {
|
|
67
|
+
await fetch('http://localhost:4003/hello?num=42');
|
|
68
|
+
await fetch('http://localhost:4004/hello?num=42');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Run benchmarks
|
|
72
|
+
const calyxResult = await runAutocannon(4003, 'calyx (Bun Native with Guards/Pipes/Interceptors)');
|
|
73
|
+
const nestResult = await runAutocannon(4004, 'NestJS (Express with Guards/Pipes/Interceptors)');
|
|
74
|
+
|
|
75
|
+
console.log('\n======================================================');
|
|
76
|
+
console.log('HTTP Lifecycle Pipeline (Guards/Pipes/Interceptors) Comparison');
|
|
77
|
+
console.log('======================================================');
|
|
78
|
+
console.log(`calyx (Bun Native):`);
|
|
79
|
+
console.log(` Requests/Sec (Avg): ${calyxResult.requests.average.toFixed(0)}`);
|
|
80
|
+
console.log(` Latency (Avg ms): ${calyxResult.latency.average.toFixed(2)}`);
|
|
81
|
+
console.log(` Throughput (Mb/s): ${(calyxResult.throughput.average / 1024 / 1024).toFixed(2)}`);
|
|
82
|
+
console.log(`------------------------------------------------------`);
|
|
83
|
+
console.log(`NestJS (Express on Bun):`);
|
|
84
|
+
console.log(` Requests/Sec (Avg): ${nestResult.requests.average.toFixed(0)}`);
|
|
85
|
+
console.log(` Latency (Avg ms): ${nestResult.latency.average.toFixed(2)}`);
|
|
86
|
+
console.log(` Throughput (Mb/s): ${(nestResult.throughput.average / 1024 / 1024).toFixed(2)}`);
|
|
87
|
+
console.log(`------------------------------------------------------`);
|
|
88
|
+
|
|
89
|
+
const speedup = calyxResult.requests.average / nestResult.requests.average;
|
|
90
|
+
console.log(`calyx is ${speedup.toFixed(2)}x FASTER in requests/sec than NestJS with full pipeline!`);
|
|
91
|
+
console.log('======================================================\n');
|
|
92
|
+
|
|
93
|
+
} finally {
|
|
94
|
+
// Kill processes
|
|
95
|
+
console.log('Shutting down server processes...');
|
|
96
|
+
calyxProc.kill();
|
|
97
|
+
nestProc.kill();
|
|
98
|
+
console.log('Servers shut down successfully.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Query,
|
|
7
|
+
UseGuards,
|
|
8
|
+
UseInterceptors,
|
|
9
|
+
UsePipes,
|
|
10
|
+
CanActivate,
|
|
11
|
+
NestInterceptor,
|
|
12
|
+
CallHandler,
|
|
13
|
+
PipeTransform,
|
|
14
|
+
ExecutionContext,
|
|
15
|
+
calyxFactory,
|
|
16
|
+
} from '../src/index.ts';
|
|
17
|
+
|
|
18
|
+
class AuthGuard implements CanActivate {
|
|
19
|
+
canActivate(context: ExecutionContext): boolean {
|
|
20
|
+
return true; // dummy guard
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TransformInterceptor implements NestInterceptor {
|
|
25
|
+
async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
|
|
26
|
+
const res = await next.handle();
|
|
27
|
+
return { ...res, intercepted: true };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class ParseIntPipe implements PipeTransform {
|
|
32
|
+
transform(value: any) {
|
|
33
|
+
return Number(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Controller('hello')
|
|
38
|
+
@UseGuards(AuthGuard)
|
|
39
|
+
@UseInterceptors(TransformInterceptor)
|
|
40
|
+
class HelloController {
|
|
41
|
+
@Get()
|
|
42
|
+
getHello(@Query('num', ParseIntPipe) num: number) {
|
|
43
|
+
return { value: num };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Module({
|
|
48
|
+
controllers: [HelloController],
|
|
49
|
+
})
|
|
50
|
+
class AppModule {}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const app = await calyxFactory.create(AppModule);
|
|
54
|
+
await app.listen(4003);
|
|
55
|
+
console.log('calyx lifecycle listening on 4003');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Module, Controller, Get, calyxFactory } from '../src/index.ts';
|
|
3
|
+
|
|
4
|
+
@Controller('hello')
|
|
5
|
+
class HelloController {
|
|
6
|
+
@Get()
|
|
7
|
+
getHello() {
|
|
8
|
+
return { message: 'hello' };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Module({
|
|
13
|
+
controllers: [HelloController],
|
|
14
|
+
})
|
|
15
|
+
class AppModule {}
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const app = await calyxFactory.create(AppModule);
|
|
19
|
+
await app.listen(4001);
|
|
20
|
+
console.log('calyx listening on 4001');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Query,
|
|
7
|
+
UseGuards,
|
|
8
|
+
UseInterceptors,
|
|
9
|
+
UsePipes,
|
|
10
|
+
CanActivate,
|
|
11
|
+
NestInterceptor,
|
|
12
|
+
CallHandler,
|
|
13
|
+
PipeTransform,
|
|
14
|
+
ExecutionContext,
|
|
15
|
+
} from '@nestjs/common';
|
|
16
|
+
import { NestFactory } from '@nestjs/core';
|
|
17
|
+
|
|
18
|
+
class AuthGuard implements CanActivate {
|
|
19
|
+
canActivate(context: ExecutionContext): boolean {
|
|
20
|
+
return true; // dummy guard
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TransformInterceptor implements NestInterceptor {
|
|
25
|
+
async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
|
|
26
|
+
// Note: NestJS interceptor handle() returns RxJS Observable, which we must import to run the map operator
|
|
27
|
+
// To keep it simple, since RxJS is installed in our devDependencies, we can do it:
|
|
28
|
+
const { map } = await import('rxjs/operators');
|
|
29
|
+
return next.handle().pipe(map((res: any) => ({ ...res, intercepted: true })));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class ParseIntPipe implements PipeTransform {
|
|
34
|
+
transform(value: any) {
|
|
35
|
+
return Number(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Controller('hello')
|
|
40
|
+
@UseGuards(AuthGuard)
|
|
41
|
+
@UseInterceptors(TransformInterceptor)
|
|
42
|
+
class HelloController {
|
|
43
|
+
@Get()
|
|
44
|
+
getHello(@Query('num', ParseIntPipe) num: number) {
|
|
45
|
+
return { value: num };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Module({
|
|
50
|
+
controllers: [HelloController],
|
|
51
|
+
})
|
|
52
|
+
class AppModule {}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const app = await NestFactory.create(AppModule, { logger: false });
|
|
56
|
+
await app.listen(4004);
|
|
57
|
+
console.log('NestJS lifecycle listening on 4004');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Module, Controller, Get } from '@nestjs/common';
|
|
3
|
+
import { NestFactory } from '@nestjs/core';
|
|
4
|
+
|
|
5
|
+
@Controller('hello')
|
|
6
|
+
class HelloController {
|
|
7
|
+
@Get()
|
|
8
|
+
getHello() {
|
|
9
|
+
return { message: 'hello' };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Module({
|
|
14
|
+
controllers: [HelloController],
|
|
15
|
+
})
|
|
16
|
+
class AppModule {}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const app = await NestFactory.create(AppModule, { logger: false });
|
|
20
|
+
await app.listen(4002);
|
|
21
|
+
console.log('NestJS listening on 4002');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Controllers & Routing
|
|
2
|
+
|
|
3
|
+
calyx's controller layer allows routing HTTP requests to class methods using decorators. It mirrors NestJS's request-mapping syntax but operates natively on Bun's ultra-fast HTTP server, bypassing Node.js compatibility wrappers or middleware overhead.
|
|
4
|
+
|
|
5
|
+
## Creating a Controller
|
|
6
|
+
|
|
7
|
+
Controllers are decorated with `@Controller(prefix?: string)`. Each method handles a specific route defined by HTTP method decorators.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Controller, Get, Post, Body } from '@martel/calyx';
|
|
11
|
+
|
|
12
|
+
@Controller('items')
|
|
13
|
+
export class ItemsController {
|
|
14
|
+
@Get()
|
|
15
|
+
findAll() {
|
|
16
|
+
return ['item1', 'item2'];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Post()
|
|
20
|
+
create(@Body() createDto: any) {
|
|
21
|
+
return { success: true, data: createDto };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## HTTP Method Decorators
|
|
29
|
+
|
|
30
|
+
calyx supports the standard HTTP verbs matching NestJS:
|
|
31
|
+
|
|
32
|
+
* `@Get(path?: string)`
|
|
33
|
+
* `@Post(path?: string)`
|
|
34
|
+
* `@Put(path?: string)`
|
|
35
|
+
* `@Delete(path?: string)`
|
|
36
|
+
* `@Patch(path?: string)`
|
|
37
|
+
* `@Options(path?: string)`
|
|
38
|
+
* `@Head(path?: string)`
|
|
39
|
+
* `@All(path?: string)`
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Route Parameters and Request Decorators
|
|
44
|
+
|
|
45
|
+
Method arguments can extract parts of the request payload using parameter decorators:
|
|
46
|
+
|
|
47
|
+
| Decorator | NestJS Match | Description |
|
|
48
|
+
| :--- | :--- | :--- |
|
|
49
|
+
| `@Req()` / `@Request()` | `req` | Exposes the raw Bun `Request` object. |
|
|
50
|
+
| `@Res()` / `@Response()` | `res` | Exposes the `calyxResponse` wrapper for manual responses. |
|
|
51
|
+
| `@Param(key?: string)` | `params[key]` | Extracts path parameters (e.g. `@Param('id')`). |
|
|
52
|
+
| `@Query(key?: string)` | `query[key]` | Extracts query parameters (e.g. `@Query('limit')`). |
|
|
53
|
+
| `@Body(key?: string)` | `body[key]` | Extracts request body payload (parsed JSON/urlencoded). |
|
|
54
|
+
| `@Headers(key?: string)` | `headers[key]` | Extracts request headers. |
|
|
55
|
+
|
|
56
|
+
### Example
|
|
57
|
+
```typescript
|
|
58
|
+
@Get(':userId/posts/:postId')
|
|
59
|
+
getPost(
|
|
60
|
+
@Param('userId') userId: string,
|
|
61
|
+
@Param('postId') postId: string,
|
|
62
|
+
@Query('verbose') verbose?: string
|
|
63
|
+
) {
|
|
64
|
+
return { userId, postId, verbose: verbose === 'true' };
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Response Customization
|
|
71
|
+
|
|
72
|
+
### Status Codes (`@HttpCode`)
|
|
73
|
+
By default, the response status code is `200` (except `POST` requests which default to `201`). Override this with `@HttpCode(code)`:
|
|
74
|
+
```typescript
|
|
75
|
+
@Post()
|
|
76
|
+
@HttpCode(204)
|
|
77
|
+
create() {
|
|
78
|
+
// Returns HTTP 204 No Content
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Custom Headers (`@Header`)
|
|
83
|
+
Define custom static response headers:
|
|
84
|
+
```typescript
|
|
85
|
+
@Get('custom-header')
|
|
86
|
+
@Header('Cache-Control', 'none')
|
|
87
|
+
getWithHeader() {
|
|
88
|
+
return 'No Cache';
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Redirects (`@Redirect`)
|
|
93
|
+
Redirects request to a specified URL with an optional status code (defaults to `302` Found):
|
|
94
|
+
```typescript
|
|
95
|
+
@Get('legacy-path')
|
|
96
|
+
@Redirect('/new-path', 301)
|
|
97
|
+
legacy() {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Manual Response Processing
|
|
103
|
+
|
|
104
|
+
If you need full control over the response, inject `@Res()`:
|
|
105
|
+
```typescript
|
|
106
|
+
@Get('manual')
|
|
107
|
+
manualResponse(@Res() res: calyxResponse) {
|
|
108
|
+
res.status(202).set('X-Manual', 'true').json({ success: true });
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
*Note: If you use `@Res()`, you are responsible for calling `res.send()` or `res.json()`. Otherwise, the request will hang.*
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Performance Benchmarks
|
|
116
|
+
|
|
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
|
+
|
|
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.
|
|
122
|
+
|